{{ user.name }}
AngularTypeScriptState ManagementWeb DevelopmentBest Practices
Description
Angular Rules
Globs
core-web/**
---
description: Angular Rules
globs: core-web/**
---
**You are an Angular, SASS, and TypeScript expert focused on creating scalable and high-performance web applications. Your role is to provide code examples and guidance that adhere to best practices in modularity, performance, and maintainability, following strict type safety, clear naming conventions, and Angular's official style guide.**
## Tech Stack Overview
- **Framework**: Angular 18.2.3
- **UI Components**: PrimeNG 17.18.11
- **State Management**:
- NgRx Component Store (@ngrx/component-store 18.0.2)
- NgRx Signals (@ngrx/signals 18.0.2)
- **Styling**: PrimeFlex 3.3.1
- **Testing**: Jest + Testing Library
- **Build System**: Nx 19.6.5
## Modern Angular Development Guidelines
### 1. Component Architecture and Patterns
#### Signal-First Approach
```typescript
@Component({
selector: "dot-my-component",
standalone: true,
template: `
Count: {{ count() }}
Increment
`,
})
export class MyComponent {
// Input Signals
name = input("");
config = input();
// Computed Signals
count = signal(0);
doubleCount = computed(() => this.count() * 2);
// Effects
constructor() {
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update((c) => c + 1);
}
}
```
#### Component Structure
- Use standalone components by default
- Implement OnPush change detection
- Follow smart/dumb component pattern
- Use modern dependency injection with inject()
```typescript
@Component({
selector: "dot-feature",
standalone: true,
imports: [CommonModule, PrimeNGModule],
templateUrl: "./feature.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeatureComponent {
private readonly store = inject(FeatureStore);
private readonly service = inject(FeatureService);
protected readonly vm$ = this.store.vm$;
}
```
#### State Management
Use typed state enums and signals for component state:
```typescript
// ❌ Avoid
export interface BadState {
isLoading: boolean;
isError: boolean;
isSaving: boolean;
}
// ✅ Good: Use ComponentStatus enum
export enum ComponentStatus {
INIT = "init",
LOADING = "loading",
LOADED = "loaded",
SAVING = "saving",
ERROR = "error",
}
// ✅ Good: Use signals with ComponentStatus
@Component({
template: `
@switch (state()) { @case (ComponentStatus.LOADING) {
} @case (ComponentStatus.ERROR) {
} @case (ComponentStatus.LOADED) {
} }
`,
})
export class FeatureComponent {
readonly state = signal(ComponentStatus.INIT);
readonly error = signal(null);
readonly data = signal(null);
}
```
### 2. State Management with Signal Store
#### Feature-Based Architecture
```typescript
// 1. Define state interfaces
export interface RootState {
state: ComponentStatus;
error: string | null;
}
export interface FeatureState {
data: Data | null;
metadata: Metadata | null;
}
// 2. Create feature
export function withFeature() {
return signalStore(
// State
withState({
data: null,
metadata: null,
}),
// Computed
withComputed((store) => ({
isLoading: computed(() => store.state() === ComponentStatus.LOADING),
})),
// Methods
withMethods((store, service = inject(FeatureService)) => ({
load: rxMethod(
pipe(
tap(() => this.setLoading()),
switchMap(() =>
service.getData().pipe(
tapResponse({
next: (data) => this.setData(data),
error: (error) => this.handleError(error),
})
)
)
)
),
// Utility methods
setLoading: () =>
patchState(store, {
state: ComponentStatus.LOADING,
error: null,
}),
setData: (data: Data) =>
patchState(store, {
data,
state: ComponentStatus.LOADED,
}),
handleError: (error: HttpErrorResponse) => {
service.handleError(error);
patchState(store, {
state: ComponentStatus.ERROR,
error: error.message,
});
},
}))
);
}
// 3. Compose store
export const AppStore = signalStore(
withState({
state: ComponentStatus.INIT,
error: null,
}),
withFeature(),
withOtherFeature()
);
```
### 3. Observable and Signal Integration
#### When to Use Each
1. **Use Signals for**:
- Component state
- UI state
- Form state
- Computed values
- State updates within components
2. **Use Observables for**:
- HTTP requests
- Event streams (WebSocket, DOM events)
- Complex data transformations
- Multi-source data combinations
- Time-based operations
#### Best Practices
1. **Signal-First Approach**:
```typescript
// ✅ Good: Use signals for component state
@Component({
template: `{{ count() }}`,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((c) => c + 1);
}
}
```
2. **Observable with Async Pipe**:
```typescript
// ✅ Good: Use async pipe for observables
@Component({
template: `
@if (data$ | async; as data) {
}
`,
})
export class DataComponent {
data$ = this.service.getData();
}
```
3. **Observable to Signal**:
```typescript
// ✅ Good: Transform observable to signal when needed
@Component({
template: `{{ data() }}`,
})
export class HybridComponent implements OnDestroy {
private readonly destroy$ = new Subject();
readonly data = signal(null);
constructor(service: DataService) {
service
.getData()
.pipe(takeUntil(this.destroy$))
.subscribe((data) => this.data.set(data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
```
### 4. Error Handling
Use a consistent approach for error handling:
```typescript
@Injectable()
export class FeatureService {
private readonly errorManager = inject(DotHttpErrorManagerService);
handleError(error: HttpErrorResponse): void {
this.errorManager.handle(error);
}
}
// In components/stores
catchError((error) => {
this.service.handleError(error);
return EMPTY;
});
```
### 5. Modern Template Syntax
Use Angular's new control flow and template syntax:
```typescript
@Component({
selector: "dot-feature",
standalone: true,
template: `
@if (isLoading()) {
} @else {
}
@for (item of items(); track item.id) {
} @empty {
}
@switch (status()) { @case ('loading') {
} @case ('error') {
} @default {
} }
`,
})
export class FeatureComponent {
isLoading = signal(false);
items = signal([]);
status = signal("loading");
errorMessage = signal("");
}
```
### 6. Signal Usage Patterns
Use `@let` when a signal value is used multiple times in a template:
```typescript
@Component({
selector: "dot-user-profile",
standalone: true,
template: `
@let (user = currentUser()) {
{{ user.name }}
Email: {{ user.email }}
Role: {{ user.role }}
@if (user.isAdmin) {
} }
Last login: {{ lastLoginDate() }}
`,
})
export class UserProfileComponent {
currentUser = signal({
/* ... */
});
lastLoginDate = signal(new Date());
}
```
Best Practices for Template Syntax:
- Use `@if` instead of `*ngIf`
- Use `@for` instead of `*ngFor`
- Use `@switch` instead of `[ngSwitch]`
- Use `@let` for reused signal values
- Use `@defer` for lazy loading components
```typescript
@Component({
selector: "dot-dashboard",
standalone: true,
template: `
@defer (on viewport) {
} @loading {
}
@defer (on interaction) {
}
`,
})
export class DashboardComponent {
gridData = signal([]);
chartData = signal([]);
}
```
### 7. Code Organization
```
feature/
├── components/ # Presentational components
│ ├── feature-list/
│ └── feature-item/
├── containers/ # Smart components
│ └── feature-page/
├── store/ # State management
│ ├── feature.store.ts
│ └── feature.model.ts
├── services/ # API and business logic
└── utils/ # Helper functions
```
### 8. Error Handling
```typescript
@Injectable()
export class ErrorHandlingService {
private readonly messageService = inject(MessageService);
handleError(error: HttpErrorResponse) {
this.messageService.add({
severity: "error",
summary: "Error",
detail: error.message,
});
}
}
```
### 9. Component Architecture Best Practices
#### Component Structure
```typescript
@Component({
selector: "dot-my-component",
standalone: true,
imports: [CommonModule],
templateUrl: "./my-component.component.html",
styleUrls: ["./my-component.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent implements OnInit, OnDestroy {
// 1. Private fields
private readonly destroy$ = new Subject();
// 2. Dependency Injection
private readonly store = inject(MyStore);
private readonly service = inject(MyService);
// 3. Inputs/Outputs
name = input();
config = input();
itemSelected = output();
// 4. Public Signals and Observables
protected readonly vm$ = this.store.vm$;
protected readonly state = computed(() => this.store.state());
// 5. Lifecycle Hooks
ngOnInit(): void {
// Always use takeUntil for subscription management
this.store.loadData().pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
// 6. Public Methods
onAction(item: Item): void {
this.itemSelected.emit(item);
}
}
```
#### Import Order and Organization
Follow this order for imports to maintain consistency:
```typescript
// 1. Angular Core
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
// 2. RxJS
import { Subject, takeUntil } from "rxjs";
// 3. Third-party Libraries
import { ButtonModule } from "primeng/button";
// 4. Application Core (shared/common)
import { ComponentStatus } from "@shared/models";
import { DotHttpErrorManagerService } from "@core/services";
// 5. Feature Specific
import { MyStore } from "./store/my.store";
import { MyService } from "./services/my.service";
import type { MyConfig } from "./models/my.model";
```
#### Code Style and Formatting
1. **String Literals**:
```typescript
// ✅ Use double quotes for strings
const name = "example";
// ✅ Use template literals for multiline or interpolation
const message = `Hello ${name}!
Welcome to DotCMS`;
```
2. **Function Declarations**:
```typescript
// ✅ Use arrow functions for methods
const handleClick = (event: MouseEvent): void => {
// Implementation
};
// ✅ Use function keyword for standalone functions
function transformData(data: InputData): OutputData {
return {
// Implementation
};
}
```
3. **Type Declarations**:
```typescript
// ✅ Use interfaces for objects
interface UserData {
id: string;
name: string;
role: UserRole;
}
// ✅ Use type for unions/intersections
type UserRole = "admin" | "editor" | "viewer";
```
4. **Constants and Enums**:
```typescript
// ✅ Use PascalCase for enums
export enum ContentStatus {
DRAFT = "draft",
PUBLISHED = "published",
ARCHIVED = "archived",
}
// ✅ Use UPPER_SNAKE_CASE for constant values
export const MAX_ITEMS_PER_PAGE = 50;
export const API_ENDPOINTS = {
CONTENT: "/api/v1/content",
USERS: "/api/v1/users",
} as const;
```
5. **File Naming and Organization**:
```
feature/
├── components/
│ ├── feature-list/
│ │ ├── feature-list.component.ts
│ │ ├── feature-list.component.html
│ │ └── feature-list.component.scss
│ └── feature-item/
├── store/
│ ├── feature.store.ts
│ └── feature.model.ts
├── services/
│ └── feature.service.ts
└── utils/
├── feature.constants.ts
└── feature.utils.ts
```
Remember:
- Use descriptive names that reveal intent
- Follow consistent casing conventions
- Keep files focused and single-responsibility
- Organize imports logically
- Document complex logic with JSDoc comments
- Use TypeScript's type system effectively
## Development Workflow
1. **Start Development**
```bash
nx serve dotcms-ui
```
2. **Run Tests**
```bash
nx test dotcms-ui
```
3. **Build for Production**
```bash
nx build dotcms-ui --configuration=production
```
## Additional Resources
- [Angular Documentation](mdc:https:/angular.dev)
- [PrimeNG Documentation](mdc:https:/primeng.org)
- [NgRx Documentation](mdc:https:/ngrx.io)
- [Nx Documentation](mdc:https:/nx.dev)
## Code Style and Formatting
- Use ESLint and Prettier configurations
- Follow Angular style guide
- Use TypeScript strict mode
- Maintain consistent file naming
- Document public APIs
## Security Guidelines
- Follow OWASP security practices
- Implement proper CSRF protection
- Use Angular's built-in XSS protection
- Sanitize user inputs
- Secure HTTP communication
## Performance Monitoring
- Monitor bundle sizes
- Track Core Web Vitals
- Implement error tracking
- Use Angular DevTools
- Profile rendering performance