// Class decorators: @Component, @Directive, @Injectable, @NgModule, @Pipe
// Property decorators: @Input, @Output, @HostBinding, @ContentChild, @ViewChild
// Method decorators: @HostListener
// Parameter decorators: @Inject, @Optional, @Self, @SkipSelf
@Component({ selector: 'app-root', template: '<h1>Hello</h1>' })
export class AppComponent {}
Without decorators, Angular cannot identify what role a class plays. Class decorators define the type, while property decorators configure data flow.// Child
@Input() name: string = '';
@Input({ required: true }) id!: number; // Required in Angular 16+
// Parent template
<app-child [name]="userName" [id]="userId"></app-child>
Input values are available in ngOnInit and later. Use ngOnChanges to react to input value changes over time.
Why it matters: Tests understanding of parent-to-child communication. Shows you can accept and react to configuration data.
Real applications: Passing user data, configuration settings, feature flags, and display options to child components.
Common mistakes: Developers use Inputs for non-readonly data causing confusion. They don't use ngOnChanges when they need to react to changes. They don't make Inputs truly inputs (should not modify them).
@Output() itemSelected = new EventEmitter<Item>();
onSelect(item: Item) {
this.itemSelected.emit(item);
}
// Parent template
<app-child (itemSelected)="onItemSelect($event)"></app-child>
@Output enables child-to-parent communication. Use descriptive event names and emit only the data the parent needs.
Why it matters: Tests understanding of child-to-parent communication patterns. Shows you can create reusable components with custom events.
Real applications: Button clicks, form submissions, item selections, deletion confirmations. Any time child needs to notify parent of events.
Common mistakes: Developers emit entire objects when only a subset is needed. They use poor event names like 'change' instead of 'itemSelected'. They forget decorators are needed for event bindings to work.
@ViewChild('inputRef') input!: ElementRef;
@ViewChild(ChildComponent) child!: ChildComponent;
ngAfterViewInit() {
this.input.nativeElement.focus();
this.child.doSomething();
}
Set static: true if you need the reference in ngOnInit instead of ngAfterViewInit. Use @ViewChildren to query for multiple elements.
Why it matters: Tests understanding of accessing DOM elements and child components directly. Shows you know when direct access is appropriate.
Real applications: Focus management on input fields, triggering child methods, direct DOM manipulation when needed, accessing chart libraries or third-party components.
Common mistakes: Developers access @ViewChild in ngOnInit (not available yet). They don't use typed templates coercion. They overuse direct access when data binding would work better.
// Content projection
<app-card>
<app-header>Title</app-header>
</app-card>
// In CardComponent
@ContentChild(HeaderComponent) header!: HeaderComponent;
Use @ContentChildren to query for multiple projected elements. Both return a QueryList that can be observed for changes.
Why it matters: Tests understanding of content projection and component composition. Shows you can build flexible wrapper components.
Real applications: Card components with projected headers/footers, modal dialogs with projected content, accordion components with projected panels.
Common mistakes: Developers confuse @ContentChild with @ViewChild. They don't understand content projection timing (available in ngAfterContentInit). They query wrong selector or component type.
@HostListener('click', ['$event'])
onClick(event: MouseEvent) {
console.log('Host clicked!', event);
}
@HostListener('window:resize', ['$event'])
onResize(event: Event) {
this.windowWidth = window.innerWidth;
}
In modern Angular, prefer the host property in the decorator metadata over @HostListener. It provides the same functionality with less boilerplate.@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
@HostBinding('class.active') isActive = false;
@HostBinding('style.backgroundColor') bgColor = '';
@HostListener('mouseenter') onHover() {
this.bgColor = 'yellow';
this.isActive = true;
}
}
Combine @HostBinding with @HostListener to create interactive directives. The host property in the decorator metadata is the modern alternative.
Why it matters: Tests understanding of directive styling and behavior. Shows you can create reusable interactive directives.
Real applications: Highlight directives on hover, disable state styling, dynamic theme application, accessibility features like focus indicators.
Common mistakes: Developers don't pair @HostBinding with @HostListener. They use inline styles instead of class binding. They forget @HostBinding is for properties, not template references.
@Injectable({ providedIn: 'root' }) // Tree-shakable singleton
export class DataService {
constructor(private http: HttpClient) {}
}
The providedIn: 'root' option eliminates the need to add the service to a providers array. Angular automatically removes unused services during tree-shaking.
Why it matters: Tests understanding of dependency injection and service registration. Shows you can make services available throughout the app.
Real applications: Data services, HTTP clients, logging services, authentication services. Almost every service needs @Injectable with providedIn: 'root'.
Common mistakes: Developers forget @Injectable decorator entirely. They use @Injectable without providedIn. They add to providers array when providedIn is cleaner.
constructor(@Inject(API_URL) private apiUrl: string) {}
// With InjectionToken
export const API_URL = new InjectionToken<string>('api-url');
InjectionToken prevents naming collisions between providers. Always use InjectionToken instead of plain strings for better type safety and refactoring support.
Why it matters: Tests understanding of advanced dependency injection. Shows you know how to handle special injection tokens.
Real applications: Configuration values, API keys, feature flags, application-wide settings that must be injected as tokens.
Common mistakes: Developers use plain strings as injection tokens losing type safety. They don't create InjectionToken for typed non-class values. They forget @Inject when using non-class tokens.
// ViewChildren: queries own template elements
@ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;
// ContentChildren: queries projected elements
@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;
ngAfterViewInit() {
this.items.changes.subscribe(() => console.log('Items changed'));
}
Use the changes Observable on QueryList to react to dynamic additions or removals. Both decorators support the descendants option for deep querying.Why it matters: Tests understanding of querying multiple elements dynamically. Shows you can handle collections of components/elements.
Real applications: Tab components with multiple tab panels, lists with dynamic items, form arrays, menu items that change over time.
Common mistakes: Developers use @ViewChild/@ContentChild when they need @ViewChildren/@ContentChildren. They don't subscribe to .changes for dynamic lists. They iterate QueryList thinking it's an array.
// ViewChild: queries own template
@ViewChild('myInput') input!: ElementRef;
// ContentChild: queries projected content
@ContentChild(HeaderComponent) header!: HeaderComponent;
// In parent template
<app-card>
<app-header>Projected!</app-header> <!-- queried by ContentChild -->
</app-card>
Think of ViewChild as querying your own template and ContentChild as querying what others put inside your component tags.
Why it matters: Tests understanding of template vs projected content. Shows you know component composition patterns.
Real applications: ViewChild accesses form inputs or child components you created. ContentChild accesses elements the parent projected in using ng-content.
Common mistakes: Developers mix up which decorator to use. They can't find projected content because they use @ViewChild. They think @ContentChild queries only direct children.
constructor(
@Optional() private analytics: AnalyticsService
) {}
trackEvent(name: string) {
// Only track if analytics service is available
this.analytics?.track(name);
}
Always check for null before using optional dependencies. Use the optional chaining operator (?.) for safe access.
Why it matters: Tests understanding of optional dependencies. Shows you can handle gracefully missing services.
Real applications: Optional analytics services, feature-specific services that may not be installed, plugin architectures, testing with optional mocks.
Common mistakes: Developers don't use @Optional causing injection errors. They forget null checks on optional dependencies. They use optional for everything instead of just truly optional features.
@Pipe({
name: 'truncate',
standalone: true,
pure: true // default
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50): string {
return value.length > limit ? value.substring(0, limit) + '...' : value;
}
}
// Usage: {{ longText | truncate:30 }}
Pipes can be standalone by setting standalone: true for use without NgModules. Pure pipes are more performant as they only run when input values change.
Why it matters: Tests understanding of pipe creation and optimization. Shows you can extend template functionality.
Real applications: Date pipes with custom formats, currency pipes with specific locales, text transformation pipes (uppercase, truncate), filter pipes.
Common mistakes: Developers create impure pipes unnecessarily harming performance. They forget to implement PipeTransform. They don't understand the pure flag changes when pipe runs.
@Component({ selector: 'app-button' })
export class ButtonComponent {
constructor(@Attribute('size') public size: string) {
console.log(size); // 'large'
}
}
// Usage: <app-button size="large"></app-button>
// Note: size is read once, NOT bound. [size]="var" won't update it.
The value is always a string or undefined. Use @Input instead if you need dynamic values that change over time.
Why it matters: Tests understanding of static vs dynamic attribute binding. Shows you know performance and change detection implications.
Real applications: Reading data attributes for configuration, reading custom attributes set by build tools, settings that never change during component lifetime.
Common mistakes: Developers use @Attribute for dynamic values then wonder why they don't update. They forget it's read at creation time only. They use @Attribute when plain @Input would work.
// Directive: adds behavior to existing elements
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
@HostListener('mouseenter') onHover() { /* highlight logic */ }
}
// Component: has its own template and view
@Component({
selector: 'app-card',
template: '<div class="card"><ng-content></ng-content></div>'
})
export class CardComponent { }
Use @Directive when you want to add behavior to existing elements. Use @Component when you need a reusable UI element with its own template.
Why it matters: Tests understanding of decorator hierarchy and when to use each. Shows you can build both reusable components and attribute behaviors.
Real applications: Directives: [appHighlight], [appDebounce], [appClickOutside]. Components:
Common mistakes: Developers use @Component when @Directive would suffice losing reusability. They add templates to directives unnecessarily. They don't understand @Component extends @Directive functionality.