projectrules.ai

{{ 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
{{ user.name }}