Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Angular Architecture

Module Overview

Estimated Time: 4-5 hours | Difficulty: Advanced | Prerequisites: Module 11
As applications grow, proper architecture becomes critical. A 5-component demo app can get away with messy structure. A 500-component enterprise app cannot — without clear patterns, you end up with components that are tangled together, impossible to test in isolation, and terrifying to refactor. This module covers advanced patterns for state management, component design, and application structure that keep large codebases maintainable. What You’ll Learn:
  • Smart vs Presentational components
  • Container pattern
  • Facade pattern for state management
  • Feature modules and domain-driven design
  • Dynamic components
  • Content projection patterns

Smart vs Presentational Components

┌─────────────────────────────────────────────────────────────────────────┐
│              Component Classification                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   SMART (Container) Components        PRESENTATIONAL (Dumb) Components  │
│   ─────────────────────────────       ───────────────────────────────   │
│   • Know about services               • Pure, no side effects           │
│   • Manage state                      • Only know about inputs/outputs  │
│   • Handle business logic             • Highly reusable                 │
│   • Coordinate child components       • Easy to test                    │
│   • Usually pages/features            • Usually UI elements             │
│                                                                          │
│   Example:                             Example:                          │
│   ┌─────────────────────────┐        ┌─────────────────────────┐       │
│   │   UsersPageComponent    │        │    UserCardComponent    │       │
│   │   ─────────────────     │        │    ────────────────     │       │
│   │   - injects UserService │        │    - @Input() user      │       │
│   │   - loads users         │        │    - @Output() select   │       │
│   │   - handles CRUD        │        │    - pure template      │       │
│   └─────────────────────────┘        └─────────────────────────┘       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Presentational Component

// user-card.component.ts
@Component({
  selector: 'app-user-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="card" (click)="cardClicked()">
      <img [src]="user().avatar" [alt]="user().name" />
      <h3>{{ user().name }}</h3>
      <p>{{ user().email }}</p>
      <span class="role">{{ user().role }}</span>
    </div>
  `
})
export class UserCardComponent {
  user = input.required<User>();
  select = output<User>();
  
  cardClicked() {
    this.select.emit(this.user());
  }
}

Smart Component

// users-page.component.ts
@Component({
  selector: 'app-users-page',
  standalone: true,
  imports: [UserCardComponent, UserFormComponent],
  template: `
    <div class="users-page">
      <header>
        <h1>Users</h1>
        <button (click)="showForm.set(true)">Add User</button>
      </header>
      
      @if (loading()) {
        <app-spinner />
      } @else {
        <div class="grid">
          @for (user of users(); track user.id) {
            <app-user-card 
              [user]="user"
              (select)="selectUser($event)"
            />
          }
        </div>
      }
      
      @if (showForm()) {
        <app-user-form
          [user]="selectedUser()"
          (save)="saveUser($event)"
          (cancel)="closeForm()"
        />
      }
    </div>
  `
})
export class UsersPageComponent implements OnInit {
  private userService = inject(UserService);
  
  users = signal<User[]>([]);
  loading = signal(false);
  selectedUser = signal<User | null>(null);
  showForm = signal(false);
  
  ngOnInit() {
    this.loadUsers();
  }
  
  loadUsers() {
    this.loading.set(true);
    this.userService.getUsers().subscribe({
      next: (users) => {
        this.users.set(users);
        this.loading.set(false);
      },
      error: () => this.loading.set(false)
    });
  }
  
  selectUser(user: User) {
    this.selectedUser.set(user);
    this.showForm.set(true);
  }
  
  saveUser(user: User) {
    const save$ = user.id 
      ? this.userService.updateUser(user.id, user)
      : this.userService.createUser(user);
    
    save$.subscribe(() => {
      this.loadUsers();
      this.closeForm();
    });
  }
  
  closeForm() {
    this.selectedUser.set(null);
    this.showForm.set(false);
  }
}

Facade Pattern

Simplify complex state management with facades. The Facade pattern provides a single, unified API that hides the complexity of multiple underlying services. Without a facade, your component might need to inject five different services, coordinate their calls, handle errors from each, and manage loading states — all in one file. With a facade, the component just calls facade.loadUsers() and reads facade.users(). The facade absorbs all the coordination complexity. This is especially valuable in large teams: the facade becomes the “contract” between the UI layer and the data layer. Component developers do not need to understand how data is fetched, cached, or updated — they just use the facade’s public API.
// users.facade.ts
@Injectable({ providedIn: 'root' })
export class UsersFacade {
  private userService = inject(UserService);
  private notificationService = inject(NotificationService);
  private router = inject(Router);
  
  // State
  private _users = signal<User[]>([]);
  private _loading = signal(false);
  private _error = signal<string | null>(null);
  private _selectedUser = signal<User | null>(null);
  
  // Public selectors (read-only)
  readonly users = this._users.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();
  readonly selectedUser = this._selectedUser.asReadonly();
  
  // Computed values
  readonly userCount = computed(() => this._users().length);
  readonly activeUsers = computed(() => 
    this._users().filter(u => u.active)
  );
  
  // Actions
  loadUsers() {
    this._loading.set(true);
    this._error.set(null);
    
    this.userService.getUsers().pipe(
      catchError(error => {
        this._error.set('Failed to load users');
        return of([]);
      }),
      finalize(() => this._loading.set(false))
    ).subscribe(users => this._users.set(users));
  }
  
  selectUser(id: number) {
    const user = this._users().find(u => u.id === id);
    this._selectedUser.set(user ?? null);
  }
  
  async createUser(userData: CreateUserDto) {
    this._loading.set(true);
    
    try {
      const user = await firstValueFrom(
        this.userService.createUser(userData)
      );
      this._users.update(users => [...users, user]);
      this.notificationService.success('User created');
      this.router.navigate(['/users', user.id]);
      return user;
    } catch (error) {
      this.notificationService.error('Failed to create user');
      throw error;
    } finally {
      this._loading.set(false);
    }
  }
  
  async updateUser(id: number, updates: Partial<User>) {
    this._loading.set(true);
    
    try {
      const updated = await firstValueFrom(
        this.userService.updateUser(id, updates)
      );
      this._users.update(users =>
        users.map(u => u.id === id ? updated : u)
      );
      if (this._selectedUser()?.id === id) {
        this._selectedUser.set(updated);
      }
      this.notificationService.success('User updated');
      return updated;
    } catch (error) {
      this.notificationService.error('Failed to update user');
      throw error;
    } finally {
      this._loading.set(false);
    }
  }
  
  async deleteUser(id: number) {
    if (!confirm('Delete this user?')) return;
    
    try {
      await firstValueFrom(this.userService.deleteUser(id));
      this._users.update(users => users.filter(u => u.id !== id));
      if (this._selectedUser()?.id === id) {
        this._selectedUser.set(null);
      }
      this.notificationService.success('User deleted');
    } catch (error) {
      this.notificationService.error('Failed to delete user');
    }
  }
}
// users-page.component.ts - Much simpler with facade
@Component({
  selector: 'app-users-page',
  standalone: true,
  template: `
    @if (facade.loading()) {
      <app-spinner />
    } @else if (facade.error()) {
      <app-error [message]="facade.error()" (retry)="facade.loadUsers()" />
    } @else {
      <app-users-list 
        [users]="facade.users()"
        (select)="facade.selectUser($event)"
        (delete)="facade.deleteUser($event)"
      />
    }
  `
})
export class UsersPageComponent implements OnInit {
  facade = inject(UsersFacade);
  
  ngOnInit() {
    this.facade.loadUsers();
  }
}

Feature Module Structure

┌─────────────────────────────────────────────────────────────────────────┐
│              Feature Module Structure                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   src/                                                                   │
│   └── app/                                                               │
│       ├── core/                    # Singleton services                  │
│       │   ├── services/                                                  │
│       │   ├── guards/                                                    │
│       │   ├── interceptors/                                              │
│       │   └── core.module.ts                                            │
│       │                                                                  │
│       ├── shared/                  # Reusable components                 │
│       │   ├── components/                                                │
│       │   ├── directives/                                                │
│       │   ├── pipes/                                                     │
│       │   └── shared.module.ts                                          │
│       │                                                                  │
│       └── features/                # Feature modules                     │
│           ├── users/                                                     │
│           │   ├── components/      # Presentational                     │
│           │   ├── containers/      # Smart components                   │
│           │   ├── services/        # Feature-specific                   │
│           │   ├── models/                                                │
│           │   ├── state/           # Feature state (facade/store)       │
│           │   ├── users.routes.ts                                        │
│           │   └── index.ts         # Public API                         │
│           │                                                              │
│           └── products/                                                  │
│               └── ...                                                    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Feature Routes

// features/users/users.routes.ts
export const USERS_ROUTES: Routes = [
  {
    path: '',
    component: UsersLayoutComponent,
    children: [
      { path: '', component: UsersListComponent },
      { path: 'new', component: UserFormComponent },
      { 
        path: ':id', 
        component: UserDetailComponent,
        resolve: { user: userResolver }
      },
      { 
        path: ':id/edit', 
        component: UserFormComponent,
        resolve: { user: userResolver }
      }
    ]
  }
];

// app.routes.ts
export const routes: Routes = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  {
    path: 'users',
    loadChildren: () => import('./features/users/users.routes')
      .then(m => m.USERS_ROUTES)
  },
  {
    path: 'products',
    loadChildren: () => import('./features/products/products.routes')
      .then(m => m.PRODUCTS_ROUTES)
  }
];

Content Projection

Basic Projection

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <header class="card-header">
        <ng-content select="[card-header]" />
      </header>
      
      <div class="card-body">
        <ng-content />
      </div>
      
      <footer class="card-footer">
        <ng-content select="[card-footer]" />
      </footer>
    </div>
  `
})
export class CardComponent {}

// Usage
@Component({
  template: `
    <app-card>
      <h2 card-header>Card Title</h2>
      
      <p>This is the main content</p>
      <p>It goes in the body</p>
      
      <button card-footer>Action</button>
    </app-card>
  `
})

Conditional Projection with ngTemplateOutlet

// data-list.component.ts
@Component({
  selector: 'app-data-list',
  standalone: true,
  imports: [NgTemplateOutlet],
  template: `
    @if (loading) {
      <ng-container *ngTemplateOutlet="loadingTemplate || defaultLoading" />
    } @else if (items.length === 0) {
      <ng-container *ngTemplateOutlet="emptyTemplate || defaultEmpty" />
    } @else {
      @for (item of items; track item) {
        <ng-container 
          *ngTemplateOutlet="itemTemplate; context: { $implicit: item }"
        />
      }
    }
    
    <ng-template #defaultLoading>
      <div class="loading">Loading...</div>
    </ng-template>
    
    <ng-template #defaultEmpty>
      <div class="empty">No items found</div>
    </ng-template>
  `
})
export class DataListComponent<T> {
  @Input() items: T[] = [];
  @Input() loading = false;
  
  @ContentChild('item') itemTemplate!: TemplateRef<{ $implicit: T }>;
  @ContentChild('loading') loadingTemplate?: TemplateRef<void>;
  @ContentChild('empty') emptyTemplate?: TemplateRef<void>;
}

// Usage
@Component({
  template: `
    <app-data-list [items]="users" [loading]="isLoading">
      <ng-template #item let-user>
        <div class="user-row">
          <span>{{ user.name }}</span>
          <span>{{ user.email }}</span>
        </div>
      </ng-template>
      
      <ng-template #loading>
        <app-skeleton-rows [count]="5" />
      </ng-template>
      
      <ng-template #empty>
        <div class="no-users">
          <img src="empty.svg" />
          <p>No users yet. Create one!</p>
        </div>
      </ng-template>
    </app-data-list>
  `
})

Dynamic Components

// dynamic-modal.service.ts
@Injectable({ providedIn: 'root' })
export class ModalService {
  private viewContainerRef!: ViewContainerRef;
  
  setContainer(vcr: ViewContainerRef) {
    this.viewContainerRef = vcr;
  }
  
  open<C, R = any>(
    component: Type<C>,
    config?: { inputs?: Partial<C>; }
  ): Observable<R> {
    const componentRef = this.viewContainerRef.createComponent(component);
    
    // Set inputs
    if (config?.inputs) {
      Object.assign(componentRef.instance, config.inputs);
    }
    
    // Handle close
    const result$ = new Subject<R>();
    
    if ('close' in componentRef.instance) {
      (componentRef.instance as any).close.subscribe((result: R) => {
        result$.next(result);
        result$.complete();
        componentRef.destroy();
      });
    }
    
    return result$.asObservable();
  }
}

// confirm-dialog.component.ts
@Component({
  selector: 'app-confirm-dialog',
  standalone: true,
  template: `
    <div class="modal-backdrop" (click)="close.emit(false)">
      <div class="modal" (click)="$event.stopPropagation()">
        <h2>{{ title }}</h2>
        <p>{{ message }}</p>
        <div class="actions">
          <button (click)="close.emit(false)">Cancel</button>
          <button class="primary" (click)="close.emit(true)">Confirm</button>
        </div>
      </div>
    </div>
  `
})
export class ConfirmDialogComponent {
  @Input() title = 'Confirm';
  @Input() message = 'Are you sure?';
  @Output() close = new EventEmitter<boolean>();
}

// Usage
@Component({...})
export class SomeComponent {
  private modalService = inject(ModalService);
  
  async deleteItem(item: Item) {
    const confirmed = await firstValueFrom(
      this.modalService.open(ConfirmDialogComponent, {
        inputs: {
          title: 'Delete Item',
          message: `Delete "${item.name}"?`
        }
      })
    );
    
    if (confirmed) {
      this.itemService.delete(item.id);
    }
  }
}

Control Value Accessor

Create custom form controls that integrate seamlessly with Angular’s forms system. The ControlValueAccessor interface is the bridge between custom UI components and Angular’s FormControl. Once implemented, your custom component works with formControlName, [(ngModel)], validation, and all other form features — it becomes a first-class citizen of the forms API. This is how component libraries like Angular Material implement their input components.
// rating.component.ts
@Component({
  selector: 'app-rating',
  standalone: true,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingComponent),
      multi: true
    }
  ],
  template: `
    <div class="rating" [class.disabled]="disabled">
      @for (star of stars; track star) {
        <button
          type="button"
          (click)="rate(star)"
          (mouseenter)="hover.set(star)"
          (mouseleave)="hover.set(0)"
          [class.active]="star <= (hover() || value())"
          [disabled]="disabled"
        >

        </button>
      }
    </div>
  `
})
export class RatingComponent implements ControlValueAccessor {
  stars = [1, 2, 3, 4, 5];
  value = signal(0);
  hover = signal(0);
  disabled = false;
  
  private onChange = (value: number) => {};
  private onTouched = () => {};
  
  // Called when form sets value
  writeValue(value: number): void {
    this.value.set(value || 0);
  }
  
  // Register callback for value changes
  registerOnChange(fn: (value: number) => void): void {
    this.onChange = fn;
  }
  
  // Register callback for touch
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
  
  // Called when form enables/disables
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
  
  rate(star: number): void {
    if (this.disabled) return;
    
    this.value.set(star);
    this.onChange(star);
    this.onTouched();
  }
}

// Usage with reactive forms
@Component({
  template: `
    <form [formGroup]="form">
      <app-rating formControlName="rating" />
    </form>
  `
})
export class ReviewFormComponent {
  form = new FormGroup({
    rating: new FormControl(0, Validators.required),
    comment: new FormControl('')
  });
}

Inheritance vs Composition

// ❌ Inheritance - tightly coupled, inflexible
class BaseListComponent<T> {
  items: T[] = [];
  loading = false;
  
  load() { /* ... */ }
  delete(id: number) { /* ... */ }
}

class UsersListComponent extends BaseListComponent<User> {
  // Hard to customize, tight coupling
}

// ✅ Composition - flexible, testable
@Injectable()
export class ListState<T extends { id: number }> {
  items = signal<T[]>([]);
  loading = signal(false);
  
  setItems(items: T[]) { this.items.set(items); }
  addItem(item: T) { this.items.update(i => [...i, item]); }
  removeItem(id: number) { this.items.update(i => i.filter(x => x.id !== id)); }
  updateItem(id: number, updates: Partial<T>) {
    this.items.update(items => 
      items.map(i => i.id === id ? { ...i, ...updates } : i)
    );
  }
}

@Component({
  providers: [ListState],  // Each component gets its own instance
  template: `...`
})
export class UsersListComponent {
  state = inject(ListState<User>);
  private userService = inject(UserService);
  
  ngOnInit() {
    this.state.loading.set(true);
    this.userService.getUsers().subscribe(users => {
      this.state.setItems(users);
      this.state.loading.set(false);
    });
  }
}

Practice Exercise

Exercise: Build a Generic Data Table Component

Create a reusable data table with:
  1. Generic type support
  2. Column configuration via input
  3. Sorting and pagination
  4. Row selection
  5. Custom cell templates via content projection
// data-table.types.ts
export interface ColumnDef<T> {
  key: keyof T | string;
  header: string;
  sortable?: boolean;
  width?: string;
}

// data-table.component.ts
@Component({
  selector: 'app-data-table',
  standalone: true,
  imports: [NgTemplateOutlet],
  template: `
    <table>
      <thead>
        <tr>
          @if (selectable) {
            <th class="checkbox-col">
              <input 
                type="checkbox"
                [checked]="allSelected()"
                (change)="toggleAll()"
              />
            </th>
          }
          @for (col of columns(); track col.key) {
            <th 
              [style.width]="col.width"
              [class.sortable]="col.sortable"
              (click)="col.sortable && toggleSort(col.key)"
            >
              {{ col.header }}
              @if (sortKey() === col.key) {
                <span>{{ sortDir() === 'asc' ? '▲' : '▼' }}</span>
              }
            </th>
          }
        </tr>
      </thead>
      
      <tbody>
        @for (row of paginatedData(); track trackByFn(row)) {
          <tr [class.selected]="isSelected(row)">
            @if (selectable) {
              <td>
                <input 
                  type="checkbox"
                  [checked]="isSelected(row)"
                  (change)="toggleSelection(row)"
                />
              </td>
            }
            @for (col of columns(); track col.key) {
              <td>
                @if (getCellTemplate(col.key); as template) {
                  <ng-container 
                    *ngTemplateOutlet="template; context: { $implicit: row, column: col }"
                  />
                } @else {
                  {{ getNestedValue(row, col.key) }}
                }
              </td>
            }
          </tr>
        }
      </tbody>
    </table>
    
    @if (paginate) {
      <div class="pagination">
        <button 
          [disabled]="page() === 1"
          (click)="page.set(page() - 1)"
        >
          Previous
        </button>
        <span>Page {{ page() }} of {{ totalPages() }}</span>
        <button 
          [disabled]="page() >= totalPages()"
          (click)="page.set(page() + 1)"
        >
          Next
        </button>
      </div>
    }
  `
})
export class DataTableComponent<T extends { id: number | string }> {
  data = input.required<T[]>();
  columns = input.required<ColumnDef<T>[]>();
  selectable = input(false);
  paginate = input(false);
  pageSize = input(10);
  trackByFn = input<(item: T) => any>((item) => item.id);
  
  selectionChange = output<T[]>();
  
  sortKey = signal<string | keyof T | null>(null);
  sortDir = signal<'asc' | 'desc'>('asc');
  page = signal(1);
  selected = signal<Set<any>>(new Set());
  
  @ContentChildren('cell') cellTemplates!: QueryList<TemplateRef<any>>;
  
  private cellTemplateMap = new Map<string, TemplateRef<any>>();
  
  sortedData = computed(() => {
    const data = [...this.data()];
    const key = this.sortKey();
    
    if (!key) return data;
    
    return data.sort((a, b) => {
      const aVal = this.getNestedValue(a, key);
      const bVal = this.getNestedValue(b, key);
      const dir = this.sortDir() === 'asc' ? 1 : -1;
      
      if (aVal < bVal) return -1 * dir;
      if (aVal > bVal) return 1 * dir;
      return 0;
    });
  });
  
  paginatedData = computed(() => {
    if (!this.paginate()) return this.sortedData();
    
    const start = (this.page() - 1) * this.pageSize();
    return this.sortedData().slice(start, start + this.pageSize());
  });
  
  totalPages = computed(() => 
    Math.ceil(this.data().length / this.pageSize())
  );
  
  allSelected = computed(() => {
    const current = this.paginatedData();
    return current.length > 0 && 
           current.every(row => this.selected().has(this.trackByFn()(row)));
  });
  
  toggleSort(key: string | keyof T) {
    if (this.sortKey() === key) {
      this.sortDir.update(d => d === 'asc' ? 'desc' : 'asc');
    } else {
      this.sortKey.set(key);
      this.sortDir.set('asc');
    }
  }
  
  isSelected(row: T): boolean {
    return this.selected().has(this.trackByFn()(row));
  }
  
  toggleSelection(row: T) {
    const id = this.trackByFn()(row);
    this.selected.update(set => {
      const newSet = new Set(set);
      if (newSet.has(id)) {
        newSet.delete(id);
      } else {
        newSet.add(id);
      }
      return newSet;
    });
    this.emitSelection();
  }
  
  toggleAll() {
    if (this.allSelected()) {
      this.selected.set(new Set());
    } else {
      const ids = this.paginatedData().map(row => this.trackByFn()(row));
      this.selected.set(new Set(ids));
    }
    this.emitSelection();
  }
  
  getCellTemplate(key: string | keyof T): TemplateRef<any> | null {
    return this.cellTemplateMap.get(String(key)) ?? null;
  }
  
  getNestedValue(obj: any, path: string | keyof T): any {
    return String(path).split('.').reduce((o, k) => o?.[k], obj);
  }
  
  private emitSelection() {
    const selectedItems = this.data().filter(row => 
      this.selected().has(this.trackByFn()(row))
    );
    this.selectionChange.emit(selectedItems);
  }
}

// Usage
@Component({
  template: `
    <app-data-table
      [data]="users()"
      [columns]="columns"
      [selectable]="true"
      [paginate]="true"
      [pageSize]="20"
      (selectionChange)="onSelectionChange($event)"
    >
      <ng-template #cell="avatar" let-user>
        <img [src]="user.avatar" class="avatar" />
      </ng-template>
      
      <ng-template #cell="actions" let-user>
        <button (click)="edit(user)">Edit</button>
        <button (click)="delete(user)">Delete</button>
      </ng-template>
    </app-data-table>
  `
})
export class UsersTableComponent {
  columns: ColumnDef<User>[] = [
    { key: 'avatar', header: '', width: '60px' },
    { key: 'name', header: 'Name', sortable: true },
    { key: 'email', header: 'Email', sortable: true },
    { key: 'role', header: 'Role', sortable: true },
    { key: 'actions', header: 'Actions', width: '150px' }
  ];
}

Summary

1

Smart/Presentational

Separate stateful containers from pure presentational components
2

Facade Pattern

Simplify complex state with a unified API
3

Feature Modules

Organize by domain with clear boundaries
4

Content Projection

Build flexible, customizable components
5

Composition over Inheritance

Prefer composable patterns for flexibility

Interview Deep-Dive

Strong Answer: Smart components (containers) manage state, inject services, handle business logic, and coordinate child components. Presentational components (dumb) only receive data via inputs, emit events via outputs, and have no knowledge of services or application state. The benefit: presentational components are trivially testable, highly reusable, and easy to reason about because they are pure functions of their inputs.In practice, I enforce this strictly for shared/reusable components — a ButtonComponent, CardComponent, or DataTableComponent should never inject a service. For feature-level components, I am pragmatic. A UserProfileComponent that only appears in one place might inject a service directly rather than wrapping it in a container. The overhead of creating a container just to pass data down one level is not always worth the abstraction.Where it breaks down: deeply nested component trees. If a smart container needs to pass data through five levels of presentational components, you end up with “prop drilling” — every intermediate component has inputs and outputs it does not use, just passes through. The solutions are either a shared service with signals (components subscribe directly), content projection (skip intermediate levels), or a state management library like NgRx/SignalStore.The other breakdown: when a presentational component needs to trigger a complex action (like opening a modal with specific context). You can emit an event, but the parent needs to handle it, which might require injecting a modal service. At some point, the strict separation creates more indirection than clarity.Follow-up: How does this pattern interact with OnPush change detection? Answer: Beautifully. Presentational components with OnPush only re-render when their input references change. Since smart components pass data down, the presentational layer only updates when the smart component provides new data. This creates a natural performance boundary. The smart component decides when data changes; the presentational component decides how to display it. Combined with signal inputs, this means each presentational component only re-renders when its specific data changes.
Strong Answer: Inheritance in Angular is tempting but problematic. If BaseListComponent has load, filter, and paginate methods, and UsersListComponent extends it, you get tight coupling. Changing BaseListComponent can break every subclass. Testing requires understanding the full inheritance chain. And Angular has specific issues: decorators are not inherited cleanly, and lifecycle hooks in the base class can have unexpected interactions with the child.Composition alternatives: First, extract shared logic into a service. A ListState<T> service with signals for items, loading, and pagination can be provided at the component level (so each component gets its own instance). The component injects it and delegates. This is testable — you mock the service — and reusable without coupling.Second, use utility functions for pure logic. Filtering, sorting, and pagination logic can be plain functions or pure pipes that components use independently. No class hierarchy, just function calls.Third, use directives to attach shared behavior. A PaginationDirective can add pagination controls and logic to any list component. A SortableDirective can add sorting behavior to table headers. This is the most Angular-idiomatic approach.Fourth, for shared template patterns, use content projection. A DataListComponent that handles loading state, empty state, and error state accepts item templates via ng-content or ng-template. The consuming component only defines how to render each item.Follow-up: When IS inheritance acceptable in Angular? Answer: Two cases. First, abstract base classes for ControlValueAccessor implementations where the boilerplate is genuinely identical. Second, when extending third-party library components to add minor customization (though I prefer wrapping over extending). In both cases, the inheritance hierarchy should be shallow — one level deep maximum.
Strong Answer: The key architectural decision is separating behavior from rendering. The data table should handle the mechanics (sorting logic, pagination math, selection tracking) but let consumers control how cells are rendered via content projection.I would define a ColumnDef interface with key, header, sortable flag, and optional width. The component accepts data and columns as inputs. For custom cell rendering, I use ng-template with a template outlet pattern — consumers pass named templates that the table renders for specific columns. The default fallback is plain text interpolation of the cell value.For sorting: I maintain sortKey and sortDir signals internally. Clicking a header toggles the sort. The sorted data is a computed signal that derives from the input data and the sort state. For pagination: page and pageSize signals, with a paginatedData computed that slices the sorted data. The total pages is another computed.For row selection: a Set signal tracks selected IDs. The component exposes selectionChange as an output. A “select all” checkbox computes whether all visible rows are selected.The critical design decision is what NOT to include. I would not build in filtering (let consumers pre-filter data before passing it in), server-side sorting (too many assumptions about API shape), or inline editing (that is a different component). Keeping the scope tight makes the component genuinely reusable.Follow-up: How would you handle 50,000 rows in this table? Answer: I would integrate CDK virtual scrolling. Instead of rendering all rows, cdk-virtual-scroll-viewport only renders the rows visible in the viewport plus a small buffer. The data table still sorts and paginates the full dataset, but only renders a window. This changes the template from @for over paginatedData to cdkVirtualFor. The sorting and pagination computeds still work on the full array, but the DOM only has 20-30 rows at any time. If the data is too large even for client-side sorting (millions of rows), I would switch to server-side sorting and pagination, where the component emits sort/page change events and the parent fetches the appropriate page from the API.

Next Steps

Next: Server-Side Rendering

Learn SSR and hydration for better performance and SEO