# Code Style and Conventions Standards for Angular
This document outlines the coding style and conventions standards for Angular projects. Adhering to these guidelines ensures code consistency, readability, maintainability, and performance in Angular applications. These standards are designed to complement the official Angular Style Guide and incorporate modern best practices for the latest versions of Angular.
## 1. Code Formatting
Consistent code formatting is crucial for readability and maintainability. We use Prettier for automated formatting, configured to match these standards.
### 1.1. Whitespace and Indentation
* **Do This:** Use two spaces for indentation. Configure your IDE and Prettier to enforce this automatically.
* **Don't Do This:** Use tabs or mixed spaces and tabs.
* **Why:** Two-space indentation improves readability without excessive horizontal scrolling.
"""typescript
// Do This
function calculateArea(width: number, height: number): number {
return width * height;
}
// Don't Do This
function calculateArea(width: number, height: number): number {
return width * height;
}
"""
### 1.2. Line Length
* **Do This:** Keep lines of code within a maximum of 120 characters.
* **Don't Do This:** Allow lines to exceed this limit without breaking them appropriately.
* **Why:** Shorter lines improve readability, especially on smaller screens and when comparing files side-by-side.
"""typescript
// Do This
const formattedAddress = "${addressLine1}, ${addressLine2}, ${city}, ${state} ${zipCode}, ${country}";
// Don't Do This
const formattedAddress = "${addressLine1}, ${addressLine2}, ${city}, ${state} ${zipCode}, ${country, anotherProperty}";
"""
### 1.3. Blank Lines
* **Do This:** Use blank lines to separate logical blocks of code, such as between functions, classes, and sections within a function.
* **Don't Do This:** Use excessive or inconsistent blank lines.
* **Why:** Blank lines enhance readability by visually separating different parts of the code.
"""typescript
// Do This
function calculateSum(a: number, b: number): number {
return a + b;
}
function calculateProduct(a: number, b: number): number {
return a * b;
}
// Don't Do This
function calculateSum(a: number, b: number): number {return a + b;}function calculateProduct(a: number, b: number): number {return a * b;}
"""
### 1.4. Operators
* **Do This:** Use spaces around operators for readability.
* **Don't Do This:** Omit spaces around operators.
* **Why:** Consistent spacing improves code clarity and reduces the chance of misreading expressions
"""typescript
// Do This
const total = price * quantity + tax;
// Don't Do This
const total=price*quantity+tax;
"""
## 2. Naming Conventions
Consistent naming conventions make your code easier to understand.
### 2.1. General Naming
* **Do This:** Use descriptive and meaningful names.
* **Don't Do This:** Use abbreviations or single-letter names unless in simple loop counters.
* **Why:** Clear names make the code self-documenting.
"""typescript
// Do This
const userFirstName = 'John';
// Don't Do This
const fn = 'John';
"""
### 2.2. TypeScript Specific Naming
* **Do This:**
* Classes: Use PascalCase (e.g., "UserService").
* Interfaces: Use PascalCase prefixed with "I" (e.g., "IUser").
* Variables/Properties: Use camelCase (e.g., "userAge").
* Constants: Use UPPER_SNAKE_CASE (e.g., "MAX_RETRIES").
* Functions/Methods: Use camelCase (e.g., "getUser()").
* **Don't Do This:** Deviate from the established naming conventions.
* **Why:** Consistent naming provides type and purpose context, improving code readability.
"""typescript
// Do This
interface IUser {
id: number;
firstName: string;
}
class UserService {
readonly MAX_USERS = 100;
getUser(id: number): IUser {
// ...
}
}
// Don't Do This
interface user { // Incorrect. Using 'I' prefix is important in Angular.
userID: number;
userName: string;
}
class user_service { // Incorrect.
readonly maxUsers = 100;
get_user(id: number): user {
// ...
}
}
"""
### 2.3. Angular Specific Naming
* **Do This:**
* Components: Use dash-case for selectors (e.g., "app-user-profile") and PascalCase for the class name (e.g., "UserProfileComponent"). Suffix component class names with "Component".
* Services: Use PascalCase and suffix with "Service" (e.g., "AuthService").
* Modules: Use PascalCase and suffix with "Module" (e.g., "SharedModule").
* Pipes: Use camelCase for the name and PascalCase and suffix with "Pipe" for the class name (e.g., "dateFormat" and "DateFormatPipe").
* Directives: Use camelCase for the name and PascalCase and suffix with "Directive" for the class name (e.g., "highlightText" and "HighlightTextDirective").
* **Don't Do This:** Inconsistent suffixes or naming styles for Angular artifacts.
* **Why:** Angular's architectural structure becomes more identifiable with consistent naming.
"""typescript
// Components
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
// ...
}
// Services
@Injectable({
providedIn: 'root'
})
export class AuthService {
// ...
}
// Modules
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class SharedModule { }
// Pipes
@Pipe({
name: 'dateFormat'
})
export class DateFormatPipe implements PipeTransform {
// ...
}
// Directives
@Directive({
selector: '[appHighlightText]'
})
export class HighlightTextDirective {
// ...
}
"""
## 3. Stylistic Consistency
Consistent style enhances code readability, ease of review, and collaboration.
### 3.1. Code Comments
* **Do This:**
* Include JSDoc-style comments for all classes, interfaces, methods, and properties.
* Properly document parameters and return types.
* Use comments to explain complex logic.
* Keep comments up-to-date with the code.
* **Don't Do This:**
* Write obvious comments like "// Set name" before "this.name = name;".
* Leave commented-out code in the codebase.
* **Why:** Good comments provide context and explain the 'why' behind the code.
"""typescript
/**
* User service for managing user data.
*/
@Injectable({
providedIn: 'root'
})
export class UserService {
/**
* Retrieves a user by ID.
* @param id User ID.
* @returns Observable with user data.
*/
getUser(id: number): Observable {
// Fetches user data from API
return this.http.get("/api/users/${id}");
}
}
"""
### 3.2. Template Syntax
* **Do This:**
* Use consistent spacing within templates (e.g., "{{ user.name }}").
* Group related attributes together.
* Break long lines for readability.
* **Don't Do This:** Inconsistent spacing and organization in templates.
* **Why:** Readable templates are essential for front-end maintainability.
"""html
<p>{{ user.name }}</p>
<p>{{user.name}}</p>
"""
### 3.3. RxJS Best Practices
* **Do This:**
* Unsubscribe from observables in components to prevent memory leaks using "takeUntil" or the "async" pipe.
* Use pure functions for operators like "map", "filter", and "reduce".
* Handle errors gracefully using RxJS error handling operators ("catchError", "retry").
* **Don't Do This:** Forgetting to unsubscribe from observables, using side effects in RxJS operators, or ignoring errors.
* **Why:** RxJS is fundamental in Angular; proper usage avoids common pitfalls.
"""typescript
// Do This
import { Subject, takeUntil } from 'rxjs';
@Component({ ... })
export class UserListComponent implements OnInit, OnDestroy {
users$: Observable;
private destroy$ = new Subject();
constructor(private userService: UserService) { }
ngOnInit(): void {
this.users$ = this.userService.getUsers().pipe(
takeUntil(this.destroy$)
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
//In the template:
{{user.name}}
// Don't Do This
@Component({ ... })
export class UserListComponent implements OnInit {
users: IUser[] | undefined;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.getUsers().subscribe(users => {
this.users = users; //Not unsubscribing
});
}
}
"""
### 3.4. Dependency Injection
* **Do This:** Prefer constructor injection for dependencies. Leverage the "providedIn: 'root'" option for injectable services, using "providedIn: NgModule" only when service scope needs to limited to specified module.
* **Don't Do This:** Use "@Optional" and "@Host" injectors without appropriate justification. Avoid property injection.
* **Why:** Constructor injection improves testability and makes dependencies explicit. Using "providedIn: 'root'" leads to better tree-shaking and reduces bundle size.
"""typescript
// Do This
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private http: HttpClient) { }
}
@Component({...})
export class LoginComponent {
constructor(private authService: AuthService) {}
}
// Don't Do This
@Injectable()
export class AuthService { // No providedIn. Unless specifically overriding AuthService at the module level, use "providedIn: 'root'"
constructor(private http: HttpClient) { }
}
@Component({...})
export class LoginComponent {
@Inject(AuthService) authService: AuthService; // Property Injection: Avoid
}
"""
## 4. Angular Specific Guidelines
These guidelines are specific to Angular and address components, modules, services, and other areas.
### 4.1. Components
* **Do This:**
* Keep components small and focused on a single responsibility.
* Use "@Input()" and "@Output()" for component communication.
* Use lifecycle hooks appropriately. Prioritize "OnPush" change detection when possible for performance. Use trackBy functions with "ngFor".
* **Don't Do This:** Create large, monolithic components or directly manipulate the DOM.
* **Why:** Small, focused components are easier to test, understand, and reuse.
"""typescript
// Do This
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent implements OnInit {
@Input() user: IUser | undefined;
@Output() userSelected = new EventEmitter();
ngOnInit(): void {
// Initialization logic
}
selectUser(): void {
if (this.user) {
this.userSelected.emit(this.user);
}
}
}
{{ user.name }}
export class UserListComponent {
users: IUser[] = [...];
trackByUserId(index: number, user: IUser): number {
return user.id;
}
}
// Don't Do This
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
// Excessive logic related to multiple modules, services, etc.
}
"""
### 4.2. Modules
* **Do This:**
* Use feature modules to organize related components, services, and directives.
* Use a shared module for commonly used components, directives, and pipes.
* Use the "forRoot()" pattern for modules that provide services to the entire application.
* **Don't Do This:** Declare components in multiple modules or import modules unnecessarily.
* **Why:** Modular architecture improves maintainability, testability, and reusability.
"""typescript
// Feature Module: user.module.ts
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule, SharedModule], // Import SharedModule.
exports: [UserListComponent]
})
export class UserModule { }
// Shared Module: shared.module.ts
@NgModule({
declarations: [DateFormatPipe],
imports: [CommonModule],
exports: [DateFormatPipe, CommonModule] // Export CommonModule for components.
})
export class SharedModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: SharedModule,
providers: [/* Services intended to be singletons */]
};
}
}
//App Module: app.module.ts to import the module.
@NgModule({
imports: [
BrowserModule,
SharedModule.forRoot(), // Import SharedModule using forRoot()
],
...
})
export class AppModule { }
"""
### 4.3. Services
* **Do This:**
* Use services for business logic, data access, and shared state.
* Keep services focused on a single responsibility.
* Use dependency injection to provide services to components.
* **Don't Do This:** Directly instantiate services in components.
* **Why:** Services promote separation of concerns and testability.
"""typescript
// Do This
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
getData(): Observable {
return this.http.get('/api/data');
}
}
// Don't Do This
@Component({ ... })
export class DataComponent implements OnInit {
dataService: DataService; // Manual instantiation
constructor() {
this.dataService = new DataService(); // Avoid this.
}
}
"""
### 4.4. Routing
* **Do This:**
* Use lazy loading for feature modules.
* Configure routes in a separate module ("app-routing.module.ts").
* Protect routes with route guards.
* **Don't Do This:** Define all routes in the "AppModule" or neglect route protection.
* **Why:** Lazy loading improves initial load time, and route guards enhance security.
"""typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'users',
loadChildren: () => import('./user/user.module').then(m => m.UserModule),
canActivate: [AuthGuard]
},
{
path: 'login',
component: LoginComponent
},
{
path: '**',
redirectTo: 'users'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
"""
## 5. Performance Optimization
Performance is crucial, especially for large Angular applications.
### 5.1. Change Detection
* **Do This:**
* Use "ChangeDetectionStrategy.OnPush" whenever possible.
* Use immutable data structures to trigger change detection efficiently.
* Detach change detectors when necessary.
* **Don't Do This:** Rely on default change detection everywhere without considering performance impacts.
* **Why:** OnPush change detection improves rendering performance significantly.
"""typescript
// OnPush Change Detection
@Component({
selector: 'app-data-display',
templateUrl: './data-display.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataDisplayComponent implements OnChanges{
@Input() data: any;
ngOnChanges(changes: SimpleChanges) {
// Use changes to explicitly manage data updates
}
}
// Immutable Data Structures
this.data = { ...this.data, newValue: 'new' }; // Create new object instead of directly mutating
"""
### 5.2. Lazy Loading
* **Do This:** Lazy load feature modules to reduce initial bundle size and load time.
* **Don't Do This:** Load all modules eagerly, resulting in a large initial bundle.
* **Why:** Lazy loading improves application startup time.
(See Routing example in section 4.4 for example)
### 5.3. Ahead-of-Time (AOT) Compilation
* **Do This:** Use AOT compilation for production builds.
* **Don't Do This:** Rely on Just-in-Time (JIT) compilation in production.
* **Why:** AOT compilation reduces application size, improves rendering speed, and detects template errors at compile time.
Make sure your "angular.json" has the following to enable default AOT.
"""json
"build": {
"configurations": {
"production": {
"aot": true, //Enabled by default
}
}
}
"""
### 5.4. Image Optimization
* **Do This:** Optimize images by compressing them, using appropriate formats (WebP), and lazy loading them
* **Don't Do This:** Use large, unoptimized images that negatively impact page load times.
* **Why:** Image optimization decreases the load on the client and reduces bandwidth costs.
"""html
"""
## 6. Security Best Practices
Security is a critical aspect of Angular development.
### 6.1. Cross-Site Scripting (XSS)
* **Do This:**
* Sanitize user-provided content using the "DomSanitizer" to prevent XSS attacks.
* Avoid using "innerHTML" to inject content directly.
* **Don't Do This:** Trust user-provided content without sanitization.
* **Why:** Prevents malicious scripts from being injected into your application.
"""typescript
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({ ... })
export class DisplayComponent {
safeHtml: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
this.safeHtml = this.sanitizer.bypassSecurityTrustHtml('<p>User Input</p>');
}
}
"""
In the template:
"""html
"""
### 6.2. Cross-Site Request Forgery (CSRF)
* **Do This:**
* Implement CSRF protection by using tokens in your backend and Angular's "HttpClient" to include them in requests (using "HttpInterceptor").
* Set proper CORS policies.
* **Don't Do This:** Neglect CSRF protection, leaving your application vulnerable to unauthorized requests.
* **Why:** Protects users from malicious websites making unauthorized requests on their behalf.
"""typescript
// Interceptor to Add CSRF Token
@Injectable()
export class CSRFInterceptor implements HttpInterceptor {
intercept(req: HttpRequest, next: HttpHandler): Observable> {
const csrfToken = localStorage.getItem('csrfToken');
if (csrfToken) {
const cloned = req.clone({
headers: req.headers.set('X-CSRF-TOKEN', csrfToken)
});
return next.handle(cloned);
}
return next.handle(req);
}
}
"""
### 6.3. Data Security
* **Do This:**
* Avoid storing sensitive information (e.g., passwords, API keys) in the front-end code.
* Use HTTPS for all API requests.
* **Don't Do This:** Expose sensitive data or use unencrypted communication channels.
* **Why:** Protects sensitive data from unauthorized access.
### 6.4. Dependencies
* **Do This:**
* Regularly audit and update your dependencies to patch known vulnerabilities.
* Use tools like "npm audit" or "yarn audit" to identify and address security issues.
* **Don't Do This:** Use outdated dependencies with known security vulnerabilities.
* **Why:** Reduces risks associated with vulnerable third-party code.
## 7. Error Handling
Appropriate error handling provides a better user experience and makes applications more stable.
### 7.1. Centralized Error Handling
* **Do This:** Implement global error handling using "ErrorHandler".
* **Don't Do This:** Handle errors inconsistently or ignore them.
* **Why:** Provides a consistent way to handle errors across the application.
"""typescript
import { ErrorHandler, Injectable } from '@angular/core';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: any) {
console.error('An error occurred:', error.message);
// Report the error to a logging service
}
}
// Include in app.module.ts providers
@NgModule({
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
})
export class AppModule {}
"""
### 7.2. Observable Error Handling
* **Do This:** Use RxJS "catchError" operator to handle errors in observables gracefully.
* **Don't Do This:** Let errors propagate without handling, which can crash the application.
* **Why:** RxJS error handling provides a controlled way to handle errors in asynchronous operations.
"""typescript
getData(): Observable {
return this.http.get('/api/data').pipe(
catchError(error => {
console.error('Error fetching data', error);
return throwError('Something went wrong');
})
);
}
"""
## 8. Testing
Comprehensive testing is essential for delivering robust and reliable Angular applications.
### 8.1. Unit Testing
* **Do This:**
* Write unit tests for all components, services, and pipes.
* Use mocks and spies to isolate units under test.
* Aim for high code coverage.
* **Don't Do This:** Skip unit testing or write superficial tests.
* **Why:** Unit tests ensure individual components work as expected.
"""typescript
// Example Unit Test with Jasmine and Karma
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('UserService', () => {
let service: UserService;
let httpClientSpy: { get: jasmine.Spy };
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
service = new UserService(httpClientSpy as any);
});
it('should return expected users (HttpClient called once)', (done: DoneFn) => {
const expectedUsers: IUser[] = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(of(expectedUsers));
service.getUsers().subscribe(
users => {
expect(users).toEqual(expectedUsers);
done();
},
done.fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
});
"""
### 8.2. End-to-End (E2E) Testing
* **Do This:** Use E2E tests to verify the application works correctly from the user's perspective.
* **Don't Do This:** Rely solely on unit tests and neglect E2E testing.
* **Why:** E2E tests ensure the entire application works correctly together.
### 8.3. Integration Testing
* **Do This:** Perform integration tests to verify interactions between modules and services.
* **Don't Do This:** Only focus on individual units without verifying how they integrate.
* **Why:** Integration tests ensure that different parts of the application work together correctly.
By adhering to these coding style and conventions standards, development teams can build Angular applications that are consistent, maintainable, performant, and secure, while leveraging the latest features and best practices of the Angular ecosystem.
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'
# Angular Guidelines Use this guidelines when working with Angular related code. ## 1. Core Architecture - **Standalone Components:** Components, directives, and pipes are standalone by default. The `standalone: true` flag is no longer required and should be omitted in new code (Angular v17+ and above). - **Strong Typing:** TypeScript types, interfaces, and models provide type safety throughout the codebase - **Single Responsibility:** Each component and service has a single, well-defined responsibility - **Rule of One:** Files focus on a single concept or functionality - **Reactive State:** Signals provide reactive and efficient state management - **Dependency Injection:** Angular's DI system manages service instances - **Function-Based DI:** Use function-based dependency injection with the `inject()` function instead of constructor-based injection in all new code. Example: ```typescript import { inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; export class MyService { private readonly http = inject(HttpClient); // ... } ``` - **Lazy Loading:** Deferrable Views and route-level lazy loading with `loadComponent` improve performance - **Directive Composition:** The Directive Composition API enables reusable component behavior - **Standalone APIs Only:** Do not use NgModules, CommonModule, or RouterModule. Import only required standalone features/components. - **No Legacy Modules:** Do not use or generate NgModules for new features. Migrate existing modules to standalone APIs when possible. ## 2. Angular Style Guide Patterns - **Code Size:** Files are limited to 400 lines of code - **Single Purpose Files:** Each file defines one entity (component, service, etc.) - **Naming Conventions:** Symbols have consistent, descriptive names - **Folder Structure:** Code is organized by feature-based folders - **File Separation:** Templates and styles exist in their own files for components - **Property Decoration:** Input and output properties have proper decoration - **Component Selectors:** Component selectors use custom prefixes and kebab-case (e.g., `app-feature-name`) - **No CommonModule or RouterModule Imports:** Do not import CommonModule or RouterModule in standalone components. Import only the required standalone components, directives, or pipes. ## 3. Input Signal Patterns - **Signal-Based Inputs:** The `input()` function creates InputSignals: ```typescript // Current pattern readonly value = input(0); // Creates InputSignal // Legacy pattern @Input() value = 0; ``` - **Required Inputs:** The `input.required()` function marks inputs as mandatory: ```typescript readonly value = input.required<number>(); ``` - **Input Transformations:** Transformations convert input values: ```typescript readonly disabled = input(false, { transform: booleanAttribute }); readonly value = input(0, { transform: numberAttribute }); ``` - **Two-Way Binding:** Model inputs enable two-way binding: ```typescript readonly value = model(0); // Creates a model input with change propagation // Model values update with .set() or .update() increment(): void { this.value.update(v => v + 1); } ``` - **Input Aliases:** Aliases provide alternative input names: ```typescript readonly value = input(0, { alias: "sliderValue" }); ``` ## 3a. Typed Reactive Forms - **Typed Forms:** Always use strictly typed reactive forms by defining an interface for the form values and using `FormGroup<MyFormType>`, `FormBuilder.group<MyFormType>()`, and `FormControl<T>()`. - **Non-Nullable Controls:** Prefer `nonNullable: true` for controls to avoid null issues and improve type safety. - **Patch and Get Values:** Use `patchValue` and `getRawValue()` to work with typed form values. - **Reference:** See the [Angular Typed Forms documentation](https://angular.dev/guide/forms/typed-forms) for details and examples. ## 4. Component Patterns - **Naming Pattern:** Components follow consistent naming - `feature.type.ts` (e.g., `hero-list.component.ts`) - **Template Extraction:** Non-trivial templates exist in separate `.html` files - **Style Extraction:** Styles exist in separate `.css/.scss` files - **Signal-Based Inputs:** Components use the `input()` function for inputs - **Two-Way Binding:** Components use the `model()` function for two-way binding - **Lifecycle Hooks:** Components implement appropriate lifecycle hook interfaces (OnInit, OnDestroy, etc.) - **Element Selectors:** Components use element selectors (`selector: 'app-hero-detail'`) - **Logic Delegation:** Services contain complex logic - **Input Initialization:** Inputs have default values or are marked as required - **Lazy Loading:** The `@defer` directive loads heavy components or features - **Error Handling:** Try-catch blocks handle errors - **Modern Control Flow:** Templates use `@if`, `@for`, `@switch` instead of structural directives - **State Representation:** Components implement loading and error states - **Derived State:** The `computed()` function calculates derived state - **No NgModules:** Do not use or reference NgModules in new code. ## 5. Styling Patterns - **Component Encapsulation:** Components use scoped styles with proper encapsulation - **CSS Methodology:** BEM methodology guides CSS class naming when not using Angular Material - **Component Libraries:** Angular Material or other component libraries provide consistent UI elements - **Theming:** Color systems and theming enable consistent visual design - **Accessibility:** Components follow a11y standards - **Dark Mode:** Components support dark mode where appropriate ## 5a. Angular Material and Angular CDK Usage - **Standard UI Library:** Use Angular Material v3 for all standard UI components (buttons, forms, navigation, dialogs, etc.) to ensure consistency, accessibility, and alignment with Angular best practices. - **Component Development:** Build new UI components and features using Angular Material components as the foundation. Only create custom components when Material does not provide a suitable solution. - **Behavioral Primitives:** Use Angular CDK for advanced behaviors (drag-and-drop, overlays, accessibility, virtual scrolling, etc.) and for building custom components that require low-level primitives. - **Theming:** Leverage Angular Material's theming system for consistent color schemes, dark mode support, and branding. Define and use custom themes in `styles.scss` or feature-level styles as needed. - **Accessibility:** All UI components must meet accessibility (a11y) standards. Prefer Material components for built-in a11y support. When using CDK or custom components, follow WCAG and ARIA guidelines. - **Best Practices:** - Prefer Material's layout and typography utilities for spacing and text. - Use Material icons and fonts for visual consistency. - Avoid mixing multiple UI libraries in the same project. - Reference the [Angular Material documentation](https://material.angular.io) for usage patterns and updates. - **CDK Utilities:** Use Angular CDK utilities for custom behaviors, overlays, accessibility, and testing harnesses. - **Migration:** For legacy or custom components, migrate to Angular Material/CDK where feasible. ## 5b. Template Patterns - **Modern Control Flow:** Use the new Angular control flow syntax: `@if`, `@for`, `@switch` in templates. Do not use legacy structural directives such as `*ngIf`, `*ngFor`, or `*ngSwitch`. - **No Legacy Structural Directives:** Remove or migrate any usage of `*ngIf`, `*ngFor`, or `*ngSwitch` to the new control flow syntax in all new code. Legacy code should be migrated when touched. - **Referencing Conditional Results:** When using `@if`, reference the result using the `as` keyword, e.g. `@if (user(); as u) { ... }`. This is the recommended pattern for accessing the value inside the block. See the [Angular documentation](https://angular.dev/guide/templates/control-flow#referencing-the-conditional-expressions-result) for details. ## 6. Service and DI Patterns - **Service Declaration:** Services use the `@Injectable()` decorator with `providedIn: 'root'` for singletons - **Data Services:** Data services handle API calls and data operations - **Error Handling:** Services include error handling - **DI Hierarchy:** Services follow the Angular DI hierarchy - **Service Contracts:** Interfaces define service contracts - **Focused Responsibilities:** Services focus on specific tasks - **Function-Based DI:** Use function-based dependency injection with the `inject()` function instead of constructor-based injection in all new code. Example: ```typescript import { inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; export class MyService { private readonly http = inject(HttpClient); // ... } ``` ## 7. Directive and Pipe Patterns - **Attribute Directives:** Directives handle presentation logic without templates - **Host Property:** The `host` property manages bindings and listeners: ```typescript @Directive({ selector: '[appHighlight]', host: { // Host bindings '[class.highlighted]': 'isHighlighted', '[style.color]': 'highlightColor', // Host listeners '(click)': 'onClick($event)', '(mouseenter)': 'onMouseEnter()', '(mouseleave)': 'onMouseLeave()', // Static properties 'role': 'button', '[attr.aria-label]': 'ariaLabel' } }) ``` - **Selector Prefixes:** Directive selectors use custom prefixes - **Pure Pipes:** Pipes are pure when possible for better performance - **Pipe Naming:** Pipes follow camelCase naming conventions ## 8. State Management Patterns - **Signals:** Signals serve as the primary state management solution - **Component Inputs:** Signal inputs with `input()` handle component inputs - **Two-Way Binding:** Model inputs with `model()` enable two-way binding - **Local State:** Writable signals with `signal()` manage local component state - **Derived State:** Computed signals with `computed()` calculate derived state - **Side Effects:** The `effect()` function handles side effects - **Error Handling:** Signal computations include error handling - **Signal Conversion:** The `toSignal()` and `toObservable()` functions enable interoperability with RxJS ## 9. Testing Patterns - **Test Coverage:** Tests cover components and services - **Unit Tests:** Focused unit tests verify services, pipes, and components - **Component Testing:** TestBed and component harnesses test components - **Mocking:** Tests use mocking techniques for dependencies - **Test Organization:** Tests follow the AAA pattern (Arrange, Act, Assert) - **Test Naming:** Tests have descriptive names that explain the expected behavior - **Playwright Usage:** Playwright handles E2E testing with fixtures and test isolation - **Test Environment:** Test environments match production as closely as possible ## 10. Performance Patterns - **Change Detection:** Components use OnPush change detection strategy - **Lazy Loading:** Routes and components load lazily - **Virtual Scrolling:** Virtual scrolling renders long lists efficiently - **Memoization:** Memoization optimizes expensive computations - **Bundle Size:** Bundle size monitoring and optimization reduce load times - **Server-Side Rendering:** SSR improves initial load performance - **Web Workers:** Web workers handle intensive operations ## 11. Security Patterns - **XSS Prevention:** User input undergoes sanitization - **CSRF Protection:** CSRF tokens secure forms - **Content Security Policy:** CSP headers restrict content sources - **Authentication:** Secure authentication protects user accounts - **Authorization:** Authorization checks control access - **Sensitive Data:** Client-side code excludes sensitive data ## 12. Accessibility Patterns - **ARIA Attributes:** ARIA attributes enhance accessibility - **Keyboard Navigation:** Interactive elements support keyboard access - **Color Contrast:** UI elements maintain proper color contrast ratios - **Screen Readers:** Components work with screen readers - **Focus Management:** Focus management guides user interaction - **Alternative Text:** Images include alt text
# NgRx Signals Patterns This document outlines the state management patterns used in our Angular applications with NgRx Signals Store. ## 1. NgRx Signals Architecture - **Component-Centric Design:** Stores are designed around component requirements - **Hierarchical State:** State is organized in hierarchical structures - **Computed State:** Derived state uses computed values - **Declarative Updates:** State updates use patchState for immutability - **Store Composition:** Stores compose using features and providers - **Reactivity:** UIs build on automatic change detection - **Signal Interoperability:** Signals integrate with existing RxJS-based systems - **SignalMethod & RxMethod:** Use `signalMethod` for lightweight, signal-driven side effects; use `rxMethod` for Observable-based side effects and RxJS integration. When a service returns an Observable, always use `rxMethod` for side effects instead of converting to Promise or using async/await. ## 2. Signal Store Structure - **Store Creation:** The `signalStore` function creates stores - **Protected State:** Signal Store state is protected by default (`{ protectedState: true }`) - **State Definition:** Initial state shape is defined with `withState<StateType>({...})` - Root level state is always an object: `withState({ users: [], count: 0 })` - Arrays are contained within objects: `withState({ items: [] })` - **Dependency Injection:** Stores are injectable with `{ providedIn: 'root' }` or feature/component providers - **Store Features:** Built-in features (`withEntities`, `withHooks`, `signalStoreFeature`) handle cross-cutting concerns and enable store composition - **State Interface:** State interfaces provide strong typing - **Private Members:** Prefix all internal state, computed signals, and methods with an underscore (`_`). Ensure unique member names across state, computed, and methods. ```typescript withState({ count: 0, _internalCount: 0 }); withComputed(({ count, _internalCount }) => ({ doubleCount: computed(() => count() * 2), _doubleInternal: computed(() => _internalCount() * 2), })); ``` - **Member Integrity:** Store members have unique names across state, computed, and methods - **Initialization:** State initializes with meaningful defaults - **Collection Management:** The `withEntities` feature manages collections. Prefer atomic entity operations (`addEntity`, `updateEntity`, `removeEntity`, `setAllEntities`) over bulk state updates. Use `entityConfig` and `selectId` for entity identification. - **Entity Adapter Configuration:** Use `entityConfig` to configure the entity adapter for each store. Always specify the `entity` type, `collection` name, and a `selectId` function for unique entity identification. Pass the config to `withEntities<T>(entityConfig)` for strong typing and consistent entity management. ```typescript const userEntityConfig = entityConfig({ entity: type<User>(), collection: "users", selectId: (user: User) => user.id, }); export const UserStore = signalStore( { providedIn: "root" }, withState(initialState), withEntities(userEntityConfig), // ... ); ``` - **Custom Store Properties:** Use `withProps` to add static properties, observables, and dependencies. Expose observables with `toObservable`. ```typescript // Signal store structure example import { signalStore, withState, withComputed, withMethods, patchState, type, } from "@ngrx/signals"; import { withEntities, entityConfig } from "@ngrx/signals/entities"; import { computed, inject } from "@angular/core"; import { UserService } from "./user.service"; import { User } from "./user.model"; import { setAllEntities } from "@ngrx/signals/entities"; export interface UserState { selectedUserId: string | null; loading: boolean; error: string | null; } const initialState: UserState = { selectedUserId: null, loading: false, error: null, }; const userEntityConfig = entityConfig({ entity: type<User>(), collection: "users", selectId: (user: User) => user.id, }); export const UserStore = signalStore( { providedIn: "root" }, withState(initialState), withEntities(userEntityConfig), withComputed(({ usersEntities, usersEntityMap, selectedUserId }) => ({ selectedUser: computed(() => { const id = selectedUserId(); return id ? usersEntityMap()[id] : undefined; }), totalUserCount: computed(() => usersEntities().length), })), withMethods((store, userService = inject(UserService)) => ({ loadUsers: rxMethod<void>( pipe( switchMap(() => { patchState(store, { loading: true, error: null }); return userService.getUsers().pipe( tapResponse({ next: (users) => patchState(store, setAllEntities(users, userEntityConfig), { loading: false, }), error: () => patchState(store, { loading: false, error: "Failed to load users", }), }), ); }), ), ), selectUser(userId: string | null): void { patchState(store, { selectedUserId: userId }); }, })), ); ``` ## 3. Signal Store Methods - **Method Definition:** Methods are defined within `withMethods` - **Dependency Injection:** The `inject()` function accesses services within `withMethods` - **Method Organization:** Methods are grouped by domain functionality - **Method Naming:** Methods have clear, action-oriented names - **State Updates:** `patchState(store, newStateSlice)` or `patchState(store, (currentState) => newStateSlice)` updates state immutably - **Async Operations:** Methods handle async operations and update loading/error states - **Computed Properties:** `withComputed` defines derived state - **RxJS Integration:** `rxMethod` integrates RxJS streams. Use `rxMethod` for all store methods that interact with Observable-based APIs or services. Avoid using async/await with Observables in store methods. ```typescript // Signal store method patterns import { signalStore, withState, withMethods, patchState } from "@ngrx/signals"; import { inject } from "@angular/core"; import { TodoService } from "./todo.service"; import { Todo } from "./todo.model"; export interface TodoState { todos: Todo[]; loading: boolean; } export const TodoStore = signalStore( { providedIn: "root" }, withState<TodoState>({ todos: [], loading: false }), withMethods((store, todoService = inject(TodoService)) => ({ addTodo(todo: Todo): void { patchState(store, (state) => ({ todos: [...state.todos, todo], })); }, loadTodosSimple: rxMethod<void>( pipe( switchMap(() => { patchState(store, { loading: true }); return todoService.getTodos().pipe( tapResponse({ next: (todos) => patchState(store, { todos, loading: false }), error: () => patchState(store, { loading: false }), }), ); }), ), ), })), ); ``` ## 4. Entity Management - **Entity Configuration:** Entity configurations include ID selectors - **Collection Operations:** Entity operations handle CRUD operations - **Entity Relationships:** Computed properties manage entity relationships - **Entity Updates:** Prefer atomic entity operations (`addEntity`, `updateEntity`, `removeEntity`, `setAllEntities`) over bulk state updates. Use `entityConfig` and `selectId` for entity identification. ```typescript // Entity management patterns const userEntityConfig = entityConfig({ entity: type<User>(), collection: "users", selectId: (user: User) => user.id, }); export const UserStore = signalStore( withEntities(userEntityConfig), withMethods((store) => ({ addUser: signalMethod<User>((user) => { patchState(store, addEntity(user, userEntityConfig)); }), updateUser: signalMethod<{ id: string; changes: Partial<User> }>( ({ id, changes }) => { patchState(store, updateEntity({ id, changes }, userEntityConfig)); }, ), removeUser: signalMethod<string>((id) => { patchState(store, removeEntity(id, userEntityConfig)); }), setUsers: signalMethod<User[]>((users) => { patchState(store, setAllEntities(users, userEntityConfig)); }), })), ); ``` ## 5. Component Integration ### Component State Access - **Signal Properties:** Components access signals directly in templates - **OnPush Strategy:** Signal-based components use OnPush change detection - **Store Injection:** Components inject store services with the `inject` function - **Default Values:** Signals have default values - **Computed Values:** Components derive computed values from signals - **Signal Effects:** Component effects handle side effects ```typescript // Component integration patterns @Component({ standalone: true, imports: [UserListComponent], template: ` @if (userStore.users().length > 0) { <app-user-list [users]="userStore.users()"></app-user-list> } @else { <p>No users loaded yet.</p> } <div>Selected user: {{ selectedUserName() }}</div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class UsersContainerComponent implements OnInit { readonly userStore = inject(UserStore); selectedUserName = computed(() => { const user = this.userStore.selectedUser(); return user ? user.name : "None"; }); constructor() { effect(() => { const userId = this.userStore.selectedUserId(); if (userId) { console.log(`User selected: ${userId}`); } }); } ngOnInit() { this.userStore.loadUsers(); } } ``` ### Signal Store Hooks - **Lifecycle Hooks:** The `withHooks` feature adds lifecycle hooks to stores - **Initialization:** The `onInit` hook initializes stores - **Cleanup:** The `onDestroy` hook cleans up resources - **State Synchronization:** Hooks synchronize state between stores ```typescript // Signal store hooks patterns export const UserStore = signalStore( withState<UserState>({ /* initial state */ }), withMethods(/* store methods */), withHooks({ onInit: (store) => { // Initialize the store store.loadUsers(); // Return cleanup function if needed return () => { // Cleanup code }; }, }), ); ``` ## 6. Advanced Signal Patterns ### Signal Store Features - **Feature Creation:** The `signalStoreFeature` function creates reusable features - **Generic Feature Types:** Generic type parameters enhance feature reusability ```typescript function withMyFeature<T>(config: Config<T>) { return signalStoreFeature(/*...*/); } ``` - **Feature Composition:** Multiple features compose together - **Cross-Cutting Concerns:** Features handle logging, undo/redo, and other concerns - **State Slices:** Features define and manage specific state slices ```typescript // Signal store feature patterns export function withUserFeature() { return signalStoreFeature( withState<UserFeatureState>({ /* feature state */ }), withComputed((state) => ({ /* computed properties */ })), withMethods((store) => ({ /* methods */ })), ); } // Using the feature export const AppStore = signalStore( withUserFeature(), withOtherFeature(), withMethods((store) => ({ /* app-level methods */ })), ); ``` ### Signals and RxJS Integration - **Signal Conversion:** `toSignal()` and `toObservable()` convert between Signals and Observables - **Effects:** Angular's `effect()` function reacts to signal changes - **RxJS Method:** `rxMethod<T>(pipeline)` handles Observable-based side effects. Always prefer `rxMethod` for Observable-based service calls in stores. Do not convert Observables to Promises for store logic. - Accepts input values, Observables, or Signals - Manages subscription lifecycle automatically - **Reactive Patterns:** Signals combine with RxJS for complex asynchronous operations ```typescript // Signal and RxJS integration patterns import { signalStore, withState, withMethods, patchState } from "@ngrx/signals"; import { rxMethod } from "@ngrx/signals/rxjs-interop"; import { tapResponse } from "@ngrx/operators"; import { pipe, switchMap } from "rxjs"; import { inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { User } from "./user.model"; export interface UserState { users: User[]; loading: boolean; error: string | null; } export const UserStore = signalStore( { providedIn: "root" }, withState({ users: [], loading: false, error: null }), withMethods((store, http = inject(HttpClient)) => ({ loadUsers: rxMethod<void>( pipe( switchMap(() => { patchState(store, { loading: true, error: null }); return http.get<User[]>("/api/users").pipe( tapResponse({ next: (users) => patchState(store, { users, loading: false }), error: () => patchState(store, { loading: false, error: "Failed to load users", }), }), ); }), ), ), })), ); ``` ### Signal Method for Side Effects The `signalMethod` function manages side effects driven by Angular Signals within Signal Store: - **Input Flexibility:** The processor function accepts static values or Signals - **Automatic Cleanup:** The underlying effect cleans up when the store is destroyed - **Explicit Tracking:** Only the input signal passed to the processor function is tracked - **Lightweight:** Smaller bundle size compared to `rxMethod` ```typescript // Signal method patterns import { signalStore, withState, withMethods, patchState } from '@ngrx/signals'; import { signalMethod } from '@ngrx/signals'; import { inject } from '@angular/core'; import { Logger } from './logger'; interface UserPreferencesState { theme: 'light' | 'dark'; sendNotifications: boolean; const initialState: UserPreferencesState = { theme: 'light', sendNotifications: true, }; export const PreferencesStore = signalStore( { providedIn: 'root' }, withState(initialState), withProps(() => ({ logger: inject(Logger), })); withMethods((store) => ({ setSendNotifications(enabled: boolean): void { patchState(store, { sendNotifications: enabled }); }, // Signal method reacts to theme changes logThemeChange: signalMethod<'light' | 'dark'>((theme) => { store.logger.log(`Theme changed to: ${theme}`); }), setTheme(newTheme: 'light' | 'dark'): void { patchState(store, { theme: newTheme }); }, })), ); ``` ## 7. Custom Store Properties - **Custom Properties:** The `withProps` feature adds static properties, observables, and dependencies - **Observable Exposure:** `toObservable` within `withProps` exposes state as observables ```typescript withProps(({ isLoading }) => ({ isLoading$: toObservable(isLoading), })); ``` - **Dependency Grouping:** `withProps` groups dependencies for use across store features ```typescript withProps(() => ({ booksService: inject(BooksService), logger: inject(Logger), })); ``` ## 8. Project Organization ### Store Organization - **File Location:** Store definitions (`*.store.ts`) exist in dedicated files - **Naming Convention:** Stores follow the naming pattern `FeatureNameStore` - **Model Co-location:** State interfaces and models exist near store definitions - **Provider Functions:** Provider functions (`provideFeatureNameStore()`) encapsulate store providers ```typescript // Provider function pattern import { Provider } from "@angular/core"; import { UserStore } from "./user.store"; export function provideUserSignalStore(): Provider { return UserStore; } ``` ### Store Hierarchy - **Parent-Child Relationships:** Stores have clear relationships - **State Sharing:** Related components share state - **State Ownership:** Each state slice has a clear owner - **Store Composition:** Complex UIs compose multiple stores
# NgRx Signals Testing Guidelines These guidelines outline best practices for testing NgRx Signals Stores in Angular applications. ## 1. General Testing Patterns - **Public API Testing:** Tests interact with stores through their public API - **TestBed Usage:** Angular's `TestBed` instantiates and injects Signal Stores - **Dependency Mocking:** Tests mock store dependencies - **Store Mocking:** Component tests mock stores - **State and Computed Testing:** Tests assert on signal and computed property values - **Method Testing:** Tests trigger methods and assert on resulting state - **Protected State Access:** The `unprotected` utility from `@ngrx/signals/testing` accesses protected state - **Integration Testing:** Tests cover stores and components together - **Custom Extension Testing:** Tests verify custom store features ## 2. Example: Store Testing ```typescript import { TestBed } from "@angular/core/testing"; import { unprotected } from "@ngrx/signals/testing"; describe("CounterStore", () => { it("recomputes doubleCount on count changes", () => { const counterStore = TestBed.inject(CounterStore); patchState(unprotected(counterStore), { count: 10 }); expect(counterStore.doubleCount()).toBe(20); }); }); ``` --- Follow these patterns for all NgRx Signals Store tests. Use Jasmine, Angular’s latest APIs, and strong typing. For more, see the official NgRx Signals documentation.
# Angular Material Theming Guidelines (v3) These guidelines define how to implement, structure, and maintain themes using Angular Material v3 in this project. They are based on the official [Angular Material Theming Guide](https://material.angular.io/guide/theming) and tailored for consistency, scalability, and maintainability. --- ## 1. Theme Structure & Organization - **Central Theme File:** - Define all theme configuration in a single SCSS file (e.g., `src/theme/_theme-colors.scss`). - Import this file in `src/styles.scss`. - **No Inline Styles:** - Do not use inline styles or hardcoded colors in components. Always use theme variables. - **Feature-Level Theming:** - For feature-specific overrides, create a dedicated SCSS partial (e.g., `feature/_feature-theme.scss`) and import it in the main theme file. ## 2. Color System - **Material Color Palettes:** - Use Material color palettes (`mat-palette`) for primary, accent, and warn colors. - Define palettes for both light and dark themes. - **Custom Colors:** - Define custom palettes using `mat-palette` and reference them via theme variables. - **Surface & Background:** - Use Material surface and background tokens for backgrounds, cards, and containers. ## 3. Theme Definition & Application - **Create Themes:** - Use `mat-light-theme` and `mat-dark-theme` to define light and dark themes. - Example: ```scss $my-primary: mat-palette($mat-indigo); $my-accent: mat-palette($mat-pink, A200, A100, A400); $my-warn: mat-palette($mat-red); $my-theme: mat-light-theme( ( color: ( primary: $my-primary, accent: $my-accent, warn: $my-warn, ), ) ); ``` - **Apply Themes Globally:** - Use `@include angular-material-theme($my-theme);` in your global styles. - **Dark Mode:** - Define a dark theme and apply it using a CSS class (e.g., `.dark-theme`). - Example: ```scss .dark-theme { @include angular-material-theme($my-dark-theme); } ``` - Toggle dark mode by adding/removing the class on the root element. ## 4. Typography - **Material Typography Config:** - Use `mat-typography-config` to define custom typography. - Apply with `@include angular-material-typography($my-typography);`. - **Consistent Font Usage:** - Use theme typography variables in all components. ## 5. Component Theming - **Theming Mixins:** - Use Angular Material theming mixins for custom components. - Example: ```scss @use "@angular/material" as mat; @include mat.button-theme($my-theme); ``` - **Custom Component Themes:** - For custom components, define and use your own theming mixins that accept a theme config. ## 6. SCSS Usage & Best Practices - **@use Syntax:** - Use the `@use` rule for all Angular Material imports (not `@import`). - **No Direct Color Usage:** - Never use raw color values. Always use theme variables or palette functions. - **Variables Naming:** - Name theme variables descriptively (e.g., `$app-primary`, `$app-accent`). - **No !important:** - Avoid `!important` in theme styles. ## 7. Do's and Don'ts **Do:** - Centralize all theming logic in SCSS theme files - Use Material mixins and tokens for all component theming - Support both light and dark themes - Use CSS classes to toggle themes - Document custom palettes and typography in the theme file **Don't:** - Hardcode colors or typography in components - Use inline styles for theming - Use legacy `@import` for Material SCSS - Mix multiple theme definitions in a single file ## 8. Integration & Maintenance - **Import Order:** - Always import theme files before component styles in `styles.scss`. - **Upgrades:** - Review the [Angular Material changelog](https://github.com/angular/components/blob/main/CHANGELOG.md) for theming changes on upgrades. - **Documentation:** - Document all customizations and overrides in the theme file. --- For more details, see the [official Angular Material Theming Guide](https://material.angular.io/guide/theming).
# Angular Testing Guidelines (Jasmine + ng-mocks) These guidelines reflect Angular v19+ best practices, ng-mocks usage, and the official Angular testing guides: - [Testing services](https://angular.dev/guide/testing/services) - [Basics of testing components](https://angular.dev/guide/testing/components-basics) - [Component testing scenarios](https://angular.dev/guide/testing/components-scenarios) - [Testing attribute directives](https://angular.dev/guide/testing/attribute-directives) - [Testing pipes](https://angular.dev/guide/testing/pipes) - [Testing utility APIs](https://angular.dev/guide/testing/utility-apis) - [NgMocks Testing Components](https://ng-mocks.sudo.eu/api/MockComponent) - [NgMocks Testing Directives](https://ng-mocks.sudo.eu/api/MockDirective) - [NgMocks Testing Pipes](https://ng-mocks.sudo.eu/api/MockPipe) - [NgMocks Testing Services](https://ng-mocks.sudo.eu/api/MockService) - [NgMocks Mocking Providers](https://ng-mocks.sudo.eu/api/MockProvider) ## 1. General Patterns - Use Jasmine for all test specs (`.spec.ts`), following the AAA pattern (Arrange, Act, Assert). - Use Angular's TestBed and ComponentFixture for setup and DOM interaction. - **Services should be tested using TestBed, not ng-mocks.** - Prefer standalone components, strong typing, and feature-based file structure. - Use ng-mocks for mocking Angular dependencies (components, directives, pipes) in component/directive/pipe tests. - Use Angular's input() and model() for signal-based inputs in tests. - Use DebugElement and By for DOM queries. - Use spyOn and jasmine.createSpy for spies and mocks. - Use fakeAsync, tick, waitForAsync, and done for async code. - Use clear, descriptive test names and group related tests with describe. - **Use the latest ng-mocks APIs:** - Use `MockBuilder` for test bed setup (standalone components: `await MockBuilder(MyComponent)`) - Use `MockRender` to create the fixture (`fixture = MockRender(MyComponent)`) - Use `ngMocks.findInstance` to get the component instance with strong typing - Use `MockInstance.scope()` for test isolation if mocking services or component methods - Use `ngMocks.autoSpy('jasmine')` in your test setup to auto-spy all mocks (optional) ## 2. Service Testing Example (TestBed) Services should be tested using Angular's TestBed, not ng-mocks. Use provideHttpClientTesting for HTTP services. ```typescript import { TestBed } from "@angular/core/testing"; import { MyService } from "./my.service"; import { provideHttpClientTesting, HttpTestingController, } from "@angular/common/http/testing"; describe("MyService", () => { let service: MyService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ providers: [MyService, provideHttpClientTesting()], }); service = TestBed.inject(MyService); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("should call the API", () => { service.someApiCall().subscribe(); const req = httpMock.expectOne("/api/endpoint"); expect(req.request.method).toBe("GET"); req.flush({}); }); }); ``` ## 3. Component Testing Example (ng-mocks) ```typescript import { ComponentFixture } from "@angular/core/testing"; import { MockBuilder, MockRender, ngMocks, MockInstance } from "ng-mocks"; import { MyComponent } from "./my.component"; import { MyService } from "./my.service"; import { By } from "@angular/platform-browser"; describe("MyComponent", () => { let fixture: ComponentFixture; let component: MyComponent; let serviceMock: MyService; beforeEach(async () => { await MockBuilder(MyComponent).mock(MyService); fixture = MockRender(MyComponent); component = ngMocks.findInstance(MyComponent); serviceMock = ngMocks.findInstance(MyService); }); afterEach(() => MockInstance(MyService, undefined)); it("should create", () => { expect(component).toBeTruthy(); }); it("should render input value", () => { component.value.set("test"); fixture.detectChanges(); const el = fixture.debugElement.query(By.css(".value")); expect(el.nativeElement.textContent).toContain("test"); }); it("should call service on button click", () => { spyOn(serviceMock, "doSomething").and.returnValue("done"); const btn = fixture.debugElement.query(By.css("button")); btn.triggerEventHandler("click"); fixture.detectChanges(); expect(serviceMock.doSomething).toHaveBeenCalled(); }); it("should handle async service", fakeAsync(() => { spyOn(serviceMock, "load").and.returnValue(Promise.resolve(["a"])); component.load(); tick(); fixture.detectChanges(); expect(component.items()).toEqual(["a"]); })); }); ``` ## 4. Directive Testing Example ```typescript import { TestBed, ComponentFixture } from "@angular/core/testing"; import { MockBuilder, MockRender, ngMocks } from "ng-mocks"; import { Component } from "@angular/core"; import { MyDirective } from "./my.directive"; @Component({ template: ` Test `, }) class TestHost { value = "test"; } describe("MyDirective", () => { let fixture: ComponentFixture; let host: TestHost; beforeEach(async () => { await MockBuilder(TestHost).mock(MyDirective); fixture = MockRender(TestHost); host = fixture.point.componentInstance; }); it("should apply directive", () => { fixture.detectChanges(); const dir = ngMocks.findInstance(MyDirective); expect(dir).toBeTruthy(); }); }); ``` ## 5. Pipe Testing Example ```typescript import { TestBed } from "@angular/core/testing"; import { MockBuilder } from "ng-mocks"; import { MyPipe } from "./my.pipe"; describe("MyPipe", () => { let pipe: MyPipe; beforeEach(async () => { await MockBuilder(MyPipe); pipe = TestBed.inject(MyPipe); }); it("should transform value", () => { expect(pipe.transform("abc")).toBe("expected"); }); }); ``` ## 6. Utility Patterns - Use TestHelper classes for common DOM queries and actions. - Use DebugElement and By for querying and interacting with the DOM. - Use Angular’s async helpers (fakeAsync, tick, waitForAsync) for async code. - Use ng-mocks for all dependency mocking. ## 7. Testing Standalone Components, Directives, Pipes, and Providers with ng-mocks Standalone components, directives, pipes, and providers in Angular (v14+) can be tested and their dependencies mocked using ng-mocks. By default, MockBuilder will keep the class under test and mock all its dependencies. **You do not need to explicitly call `.keep()` for the class under test.** > **Note:** Only use `.keep()` if you want to keep a dependency (e.g., a child component or pipe), not the class under test itself. ### Mocking All Imports (Shallow Test) ```typescript import { MockBuilder, MockRender, ngMocks } from "ng-mocks"; import { MyStandaloneComponent } from "./my-standalone.component"; describe("MyStandaloneComponent", () => { beforeEach(async () => { await MockBuilder(MyStandaloneComponent); // mocks all imports by default, keeps the component under test }); it("should render", () => { const fixture = MockRender(MyStandaloneComponent); const component = ngMocks.findInstance(MyStandaloneComponent); expect(component).toBeTruthy(); }); }); ``` ### Keeping Specific Imports (Deep Test) If you want to keep a specific import (e.g., a pipe or dependency component), use `.keep()`: ```typescript beforeEach(async () => { await MockBuilder(MyStandaloneComponent).keep(MyDependencyComponent); }); ``` ### Reference - See the [ng-mocks guide for standalone components](https://ng-mocks.sudo.eu/guides/component-standalone/) for more details and advanced usage. --- **Follow these patterns for all Angular tests. Use Jasmine, ng-mocks, and Angular’s latest APIs. Prefer strong typing, standalone components, and feature-based structure. For more, see the official Angular testing guides.**