Angular Core Implementation Skill
Quick Start
Component Basics
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-card',
template: <div class="card"> <h2>{{ user.name }}</h2> <p>{{ user.email }}</p> <button (click)="onDelete()">Delete</button> </div> ,
styles: [ .card { border: 1px solid #ddd; padding: 16px; } ]
})
export class UserCardComponent {
@Input() user!: User;
@Output() deleted = new EventEmitter<void>();
onDelete() { this.deleted.emit(); } }
Service Creation
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' // Singleton service }) export class UserService { private apiUrl = '/api/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> { return this.http.get<User[]>(this.apiUrl); }
getUser(id: number): Observable<User> {
return this.http.get<User>(${this.apiUrl}/${id});
}
createUser(user: User): Observable<User> { return this.http.post<User>(this.apiUrl, user); } }
Dependency Injection
@Injectable() export class NotificationService { constructor( private logger: LoggerService, private config: ConfigService ) {}
notify(message: string) { this.logger.log(message); } }
Core Concepts
Lifecycle Hooks
export class UserListComponent implements OnInit, OnChanges, OnDestroy { @Input() users: User[] = [];
ngOnInit() { // Initialize component, fetch data this.loadUsers(); }
ngOnChanges(changes: SimpleChanges) { // Respond to input changes if (changes['users']) { this.onUsersChanged(); } }
ngOnDestroy() { // Cleanup subscriptions, remove listeners this.subscription?.unsubscribe(); }
private loadUsers() { /* ... / } private onUsersChanged() { / ... */ } }
Lifecycle Order:
-
ngOnChanges
-
When input properties change
-
ngOnInit
-
After first ngOnChanges
-
ngDoCheck
-
Every change detection cycle
-
ngAfterContentInit
-
After content is initialized
-
ngAfterContentChecked
-
After content is checked
-
ngAfterViewInit
-
After view is initialized
-
ngAfterViewChecked
-
After view is checked
-
ngOnDestroy
-
When component is destroyed
Modules
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms';
@NgModule({ declarations: [ UserListComponent, UserDetailComponent, UserFormComponent ], imports: [ CommonModule, FormsModule ], exports: [ UserListComponent, UserDetailComponent ] }) export class UserModule { }
Lazy Loading
const routes: Routes = [ { path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule) } ];
Advanced Patterns
Content Projection
// Parent component <app-card> <div class="header">Card Title</div> <div class="content">Card content</div> </app-card>
// Card component
@Component({
selector: 'app-card',
template: <div class="card"> <ng-content select=".header"></ng-content> <ng-content select=".content"></ng-content> <ng-content></ng-content> </div>
})
export class CardComponent { }
ViewChild and ContentChild
@Component({
selector: 'app-form',
template: <app-input #firstInput></app-input>
})
export class FormComponent implements AfterViewInit {
@ViewChild('firstInput') firstInput!: InputComponent;
ngAfterViewInit() { this.firstInput.focus(); } }
Custom Directive
@Directive({ selector: '[appHighlight]' }) export class HighlightDirective { constructor(private el: ElementRef) { this.el.nativeElement.style.backgroundColor = 'yellow'; } }
// Usage: <p appHighlight>Highlighted text</p>
Encapsulation
View Encapsulation Modes
@Component({
selector: 'app-card',
template: <div class="card">...</div>,
styles: [.card { color: blue; }],
encapsulation: ViewEncapsulation.Emulated // Default
})
export class CardComponent { }
-
Emulated (default): CSS scoped to component
-
None: Global styles
-
ShadowDom: Uses browser shadow DOM
Change Detection
OnPush Strategy
@Component({
selector: 'app-user',
template: <div>{{ user.name }}</div>,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
@Input() user!: User;
constructor(private cdr: ChangeDetectorRef) {}
manualDetection() { this.cdr.markForCheck(); } }
Provider Patterns
Multi-Provider
@NgModule({ providers: [ { provide: VALIDATORS, useValue: emailValidator, multi: true }, { provide: VALIDATORS, useValue: minLengthValidator, multi: true } ] }) export class ValidatorsModule { }
Factory Pattern
@NgModule({ providers: [ { provide: ConfigService, useFactory: (env: EnvironmentService) => { return env.production ? new ProdConfigService() : new DevConfigService(); }, deps: [EnvironmentService] } ] }) export class AppModule { }
Testing Components
describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserCardComponent] }).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should emit deleted when delete button clicked', () => { spyOn(component.deleted, 'emit'); component.user = { id: 1, name: 'John', email: 'john@example.com' }; fixture.detectChanges();
fixture.debugElement.query(By.css('button')).nativeElement.click();
expect(component.deleted.emit).toHaveBeenCalled();
}); });
Performance Optimization
-
Use OnPush: Reduces change detection cycles
-
Unsubscribe: Prevent memory leaks
-
TrackBy: Optimize *ngFor rendering
-
Lazy Load: Load modules on demand
-
Avoid property binding in templates: Use async pipe
// Bad users: User[] = [];
// Good users$ = this.userService.getUsers();
<!-- Template --> <app-user *ngFor="let user of users$ | async; trackBy: trackByUserId"> </app-user>
Best Practices
-
Smart vs Presentational: Container components handle logic
-
One Responsibility: Each component has a single purpose
-
Input/Output: Use @Input/@Output for communication
-
Services: Handle business logic and HTTP
-
DI: Always use dependency injection
-
OnDestroy: Clean up subscriptions
Resources
-
Angular Documentation
-
Angular Best Practices
-
Component Interaction