angular-component

Installation
SKILL.md

Angular Component

Modern Angular component patterns with standalone components, signals, and OnPush change detection.

Basic Component Structure

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-example',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [],
  template: `
    <div>Component content</div>
  `
})
export class ExampleComponent {}

Signal Inputs (Angular 17.1+)

Required Inputs

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h2>{{ name() }}</h2>
    <p>{{ email() }}</p>
  `
})
export class UserCardComponent {
  // Required input
  name = input.required<string>();
  email = input.required<string>();
}

// Usage
<app-user-card [name]="userName" [email]="userEmail" />

Optional Inputs with Defaults

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-badge',
  template: `
    <span [class]="'badge badge-' + variant()">
      {{ text() }}
    </span>
  `
})
export class BadgeComponent {
  text = input<string>('Badge');
  variant = input<'primary' | 'secondary' | 'success'>('primary');
}

Transformed Inputs

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-price',
  template: `<span>{{ formattedPrice() }}</span>`
})
export class PriceComponent {
  // Transform string to number
  price = input(0, {
    transform: (value: string | number) => 
      typeof value === 'string' ? parseFloat(value) : value
  });
  
  formattedPrice = computed(() => 
    new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(this.price())
  );
}

// Usage: <app-price price="99.99" />

Signal Outputs (Angular 17.3+)

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
    <button (click)="handleClick()">
      {{ label() }}
    </button>
  `
})
export class ButtonComponent {
  label = input<string>('Click me');
  clicked = output<MouseEvent>();
  
  handleClick() {
    this.clicked.emit(new MouseEvent('click'));
  }
}

// Usage
<app-button 
  label="Submit" 
  (clicked)="onSubmit($event)" 
/>

Output with Custom Event Type

import { Component, output } from '@angular/core';

interface FormSubmitEvent {
  data: Record<string, any>;
  timestamp: number;
}

@Component({
  selector: 'app-form',
  template: `...`
})
export class FormComponent {
  submitted = output<FormSubmitEvent>();
  
  onSubmit(formData: Record<string, any>) {
    this.submitted.emit({
      data: formData,
      timestamp: Date.now()
    });
  }
}

Host Bindings

Class Bindings

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  host: {
    '[class.card]': 'true',
    '[class.card-elevated]': 'elevated()',
    '[class.card-bordered]': 'bordered()'
  },
  template: `<ng-content />`
})
export class CardComponent {
  elevated = input(false);
  bordered = input(true);
}

Attribute Bindings

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-clickable',
  host: {
    '[attr.role]': '"button"',
    '[attr.tabindex]': 'disabled() ? -1 : 0',
    '[attr.aria-disabled]': 'disabled()'
  },
  template: `<ng-content />`
})
export class ClickableComponent {
  disabled = input(false);
}

Event Bindings

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-interactive',
  host: {
    '(click)': 'handleClick($event)',
    '(keydown.enter)': 'handleEnter($event)',
    '(focus)': 'handleFocus()',
    '(blur)': 'handleBlur()'
  },
  template: `<ng-content />`
})
export class InteractiveComponent {
  clicked = output<MouseEvent>();
  focused = output<void>();
  
  handleClick(event: MouseEvent) {
    this.clicked.emit(event);
  }
  
  handleEnter(event: KeyboardEvent) {
    this.clicked.emit(event as any);
  }
  
  handleFocus() {
    this.focused.emit();
  }
  
  handleBlur() {}
}

Content Projection

Single Slot Projection

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <ng-content />
    </div>
  `
})
export class CardComponent {}

// Usage
<app-card>
  <h2>Title</h2>
  <p>Content</p>
</app-card>

Multiple Slot Projection

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

// Usage
<app-card>
  <div header>Header Content</div>
  <p>Body Content</p>
  <div footer>Footer Content</div>
</app-card>

Conditional Content Projection

import { Component, input, contentChild } from '@angular/core';

@Component({
  selector: 'app-expandable',
  template: `
    <div class="header" (click)="toggle()">
      <ng-content select="[header]" />
    </div>
    @if (expanded()) {
      <div class="content">
        <ng-content />
      </div>
    }
  `
})
export class ExpandableComponent {
  expanded = input(false);
  
  toggle() {
    // Implementation
  }
}

ViewChild and ContentChild with Signals

viewChild (Angular 17.3+)

import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-autofocus',
  template: `
    <input #inputElement type="text" />
  `
})
export class AutofocusComponent {
  inputElement = viewChild<ElementRef<HTMLInputElement>>('inputElement');
  
  constructor() {
    afterNextRender(() => {
      this.inputElement()?.nativeElement.focus();
    });
  }
}

contentChild

import { Component, contentChild, Directive } from '@angular/core';

@Directive({
  selector: '[appFormField]',
  standalone: true
})
export class FormFieldDirective {}

@Component({
  selector: 'app-form-wrapper',
  template: `
    <div class="form-wrapper">
      <ng-content />
    </div>
  `
})
export class FormWrapperComponent {
  formField = contentChild(FormFieldDirective);
  
  ngAfterContentInit() {
    if (this.formField()) {
      console.log('Form field found');
    }
  }
}

Lifecycle Hooks

Modern Lifecycle with Signals

import { 
  Component, 
  input, 
  effect,
  afterNextRender,
  afterRender
} from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  template: `<p>{{ data() }}</p>`
})
export class LifecycleComponent {
  data = input.required<string>();
  
  constructor() {
    // Runs when input signals change
    effect(() => {
      console.log('Data changed:', this.data());
    });
    
    // Runs once after first render
    afterNextRender(() => {
      console.log('First render complete');
    });
    
    // Runs after every render
    afterRender(() => {
      console.log('Render complete');
    });
  }
}

Traditional Lifecycle Hooks

import { 
  Component, 
  OnInit, 
  OnDestroy, 
  AfterViewInit 
} from '@angular/core';

@Component({
  selector: 'app-traditional',
  template: `...`
})
export class TraditionalComponent implements OnInit, OnDestroy, AfterViewInit {
  ngOnInit() {
    console.log('Component initialized');
  }
  
  ngAfterViewInit() {
    console.log('View initialized');
  }
  
  ngOnDestroy() {
    console.log('Component destroyed');
  }
}

Component Composition

Container/Presentation Pattern

// Presentation Component (Dumb)
@Component({
  selector: 'app-user-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (user of users(); track user.id) {
      <app-user-card 
        [user]="user" 
        (selected)="userSelected.emit($event)" 
      />
    }
  `
})
export class UserListComponent {
  users = input.required<User[]>();
  userSelected = output<User>();
}

// Container Component (Smart)
@Component({
  selector: 'app-users-container',
  standalone: true,
  imports: [UserListComponent],
  template: `
    <app-user-list 
      [users]="users()" 
      (userSelected)="onUserSelected($event)" 
    />
  `
})
export class UsersContainerComponent {
  private userService = inject(UserService);
  users = toSignal(this.userService.getUsers(), { initialValue: [] });
  
  onUserSelected(user: User) {
    console.log('Selected:', user);
  }
}

Best Practices

  1. Always use OnPush change detection for better performance
  2. Prefer signal inputs over @Input() decorator (Angular 17.1+)
  3. Use signal outputs over @Output() decorator (Angular 17.3+)
  4. Keep components small and focused - single responsibility
  5. Use host bindings instead of :host CSS when possible
  6. Leverage content projection for flexible component APIs
  7. Avoid direct DOM manipulation - use Angular APIs
  8. Use viewChild/contentChild instead of @ViewChild/@ContentChild

Resources

Weekly Installs
2
First Seen
Jan 26, 2026