17. Advanced Components and Component Architecture¶
Learning Objectives¶
By the end of this module, you will be able to:
- Implement advanced component patterns and architectures
- Create reusable component libraries
- Use content projection and multi-slot content projection
- Implement dynamic components and component factories
- Design scalable component hierarchies
- Apply advanced lifecycle management techniques
- Implement component inheritance and composition patterns
Component Architecture Patterns¶
Container-Presenter Pattern¶
The Container-Presenter (Smart-Dumb) pattern separates components into two categories:
// Smart Component (Container)
@Component({
selector: 'app-user-container',
template: `
<app-user-list
[users]="users$ | async"
[loading]="loading$ | async"
(userSelected)="onUserSelected($event)"
(userDeleted)="onUserDeleted($event)">
</app-user-list>
`
})
export class UserContainerComponent {
users$ = this.userService.getUsers();
loading$ = this.userService.loading$;
constructor(private userService: UserService) {}
onUserSelected(user: User): void {
this.router.navigate(['/users', user.id]);
}
onUserDeleted(userId: number): void {
this.userService.deleteUser(userId);
}
}
// Dumb Component (Presenter)
@Component({
selector: 'app-user-list',
template: `
<div class="user-list">
<div *ngIf="loading" class="loading">Loading...</div>
<div *ngFor="let user of users" class="user-item">
<span>{{ user.name }}</span>
<button (click)="onSelect(user)">View</button>
<button (click)="onDelete(user.id)">Delete</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[] = [];
@Input() loading = false;
@Output() userSelected = new EventEmitter<User>();
@Output() userDeleted = new EventEmitter<number>();
onSelect(user: User): void {
this.userSelected.emit(user);
}
onDelete(userId: number): void {
this.userDeleted.emit(userId);
}
}
Component Composition Pattern¶
// Base Modal Component
@Component({
selector: 'app-modal',
template: `
<div class="modal-overlay" *ngIf="isOpen" (click)="onOverlayClick()">
<div class="modal-content" (click)="$event.stopPropagation()">
<div class="modal-header">
<ng-content select="[slot=header]"></ng-content>
<button class="close-btn" (click)="close()">×</button>
</div>
<div class="modal-body">
<ng-content></ng-content>
</div>
<div class="modal-footer" *ngIf="hasFooter">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
</div>
`,
styleUrls: ['./modal.component.scss']
})
export class ModalComponent implements OnInit {
@Input() isOpen = false;
@Input() closeOnOverlayClick = true;
@Output() closed = new EventEmitter<void>();
hasFooter = false;
ngOnInit(): void {
// Check if footer content is projected
this.hasFooter = this.hasProjectedContent('footer');
}
close(): void {
this.isOpen = false;
this.closed.emit();
}
onOverlayClick(): void {
if (this.closeOnOverlayClick) {
this.close();
}
}
private hasProjectedContent(slot: string): boolean {
// Implementation to check projected content
return true; // Simplified
}
}
// Usage
@Component({
template: `
<app-modal [isOpen]="showModal" (closed)="showModal = false">
<h2 slot="header">Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<div slot="footer">
<button (click)="confirm()">Confirm</button>
<button (click)="cancel()">Cancel</button>
</div>
</app-modal>
`
})
export class ConfirmDialogComponent {
showModal = false;
confirm(): void {
// Handle confirmation
this.showModal = false;
}
cancel(): void {
this.showModal = false;
}
}
Content Projection and ng-content¶
Single Slot Projection¶
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-content">
<ng-content></ng-content>
</div>
</div>
`,
styleUrls: ['./card.component.scss']
})
export class CardComponent {}
// Usage
@Component({
template: `
<app-card>
<h3>Card Title</h3>
<p>Card content goes here</p>
</app-card>
`
})
export class ParentComponent {}
Multi-slot Content Projection¶
@Component({
selector: 'app-article',
template: `
<article class="article">
<header class="article-header">
<ng-content select="[slot=title]"></ng-content>
<div class="article-meta">
<ng-content select="[slot=meta]"></ng-content>
</div>
</header>
<div class="article-content">
<ng-content></ng-content>
</div>
<footer class="article-footer">
<ng-content select="[slot=actions]"></ng-content>
</footer>
</article>
`,
styleUrls: ['./article.component.scss']
})
export class ArticleComponent {}
// Usage
@Component({
template: `
<app-article>
<h1 slot="title">{{ article.title }}</h1>
<div slot="meta">
<span>By {{ article.author }}</span>
<span>{{ article.publishDate | date }}</span>
</div>
<div [innerHTML]="article.content"></div>
<div slot="actions">
<button (click)="like()">Like</button>
<button (click)="share()">Share</button>
</div>
</app-article>
`
})
export class ArticleViewComponent {
@Input() article!: Article;
like(): void {
// Handle like
}
share(): void {
// Handle share
}
}
Conditional Content Projection¶
@Component({
selector: 'app-expandable-panel',
template: `
<div class="panel">
<div class="panel-header" (click)="toggle()">
<ng-content select="[slot=header]"></ng-content>
<span class="toggle-icon">{{ isExpanded ? '▼' : '▶' }}</span>
</div>
<div class="panel-content" *ngIf="isExpanded" [@slideDown]>
<ng-content></ng-content>
</div>
<div class="panel-footer" *ngIf="isExpanded && hasFooter">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
`,
animations: [
trigger('slideDown', [
transition(':enter', [
style({ height: '0', opacity: 0 }),
animate('300ms ease-in', style({ height: '*', opacity: 1 }))
]),
transition(':leave', [
animate('300ms ease-out', style({ height: '0', opacity: 0 }))
])
])
]
})
export class ExpandablePanelComponent implements AfterContentInit {
@Input() isExpanded = false;
@ContentChild('footer') footerContent!: ElementRef;
hasFooter = false;
ngAfterContentInit(): void {
this.hasFooter = !!this.footerContent;
}
toggle(): void {
this.isExpanded = !this.isExpanded;
}
}
Dynamic Components¶
Component Factory and ViewContainerRef¶
@Injectable({
providedIn: 'root'
})
export class DynamicComponentService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {}
createComponent<T>(
component: Type<T>,
viewContainerRef: ViewContainerRef,
inputs?: Partial<T>
): ComponentRef<T> {
const factory = this.componentFactoryResolver.resolveComponentFactory(component);
const componentRef = viewContainerRef.createComponent(factory);
if (inputs) {
Object.assign(componentRef.instance, inputs);
}
return componentRef;
}
}
// Host Component
@Component({
selector: 'app-dynamic-host',
template: `
<div class="dynamic-container">
<button (click)="loadComponent('alert')">Load Alert</button>
<button (click)="loadComponent('notification')">Load Notification</button>
<button (click)="clearComponents()">Clear</button>
<ng-template #dynamicHost></ng-template>
</div>
`
})
export class DynamicHostComponent {
@ViewChild('dynamicHost', { read: ViewContainerRef, static: true })
dynamicHost!: ViewContainerRef;
private componentRefs: ComponentRef<any>[] = [];
constructor(private dynamicService: DynamicComponentService) {}
loadComponent(type: string): void {
let component: Type<any>;
let inputs: any = {};
switch (type) {
case 'alert':
component = AlertComponent;
inputs = { message: 'This is an alert!', type: 'warning' };
break;
case 'notification':
component = NotificationComponent;
inputs = { title: 'Notification', message: 'You have a new message' };
break;
default:
return;
}
const componentRef = this.dynamicService.createComponent(
component,
this.dynamicHost,
inputs
);
// Subscribe to component events
if (componentRef.instance.closed) {
componentRef.instance.closed.subscribe(() => {
this.removeComponent(componentRef);
});
}
this.componentRefs.push(componentRef);
}
clearComponents(): void {
this.componentRefs.forEach(ref => ref.destroy());
this.componentRefs = [];
this.dynamicHost.clear();
}
private removeComponent(componentRef: ComponentRef<any>): void {
const index = this.componentRefs.indexOf(componentRef);
if (index > -1) {
this.componentRefs.splice(index, 1);
componentRef.destroy();
}
}
}
Portal Pattern Implementation¶
// Portal Service
@Injectable({
providedIn: 'root'
})
export class PortalService {
private portals = new Map<string, ViewContainerRef>();
registerPortal(name: string, viewContainer: ViewContainerRef): void {
this.portals.set(name, viewContainer);
}
unregisterPortal(name: string): void {
this.portals.delete(name);
}
renderToPortal<T>(
portalName: string,
component: Type<T>,
inputs?: Partial<T>
): ComponentRef<T> | null {
const portal = this.portals.get(portalName);
if (!portal) {
console.warn(`Portal ${portalName} not found`);
return null;
}
const factory = this.componentFactoryResolver.resolveComponentFactory(component);
const componentRef = portal.createComponent(factory);
if (inputs) {
Object.assign(componentRef.instance, inputs);
}
return componentRef;
}
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
}
// Portal Outlet Component
@Component({
selector: 'app-portal-outlet',
template: '<ng-template #portalHost></ng-template>'
})
export class PortalOutletComponent implements OnInit, OnDestroy {
@Input() portalName!: string;
@ViewChild('portalHost', { read: ViewContainerRef, static: true })
portalHost!: ViewContainerRef;
constructor(private portalService: PortalService) {}
ngOnInit(): void {
if (this.portalName) {
this.portalService.registerPortal(this.portalName, this.portalHost);
}
}
ngOnDestroy(): void {
if (this.portalName) {
this.portalService.unregisterPortal(this.portalName);
}
}
}
Component Libraries¶
Creating a Reusable Component Library¶
// Button Component
@Component({
selector: 'lib-button',
template: `
<button
[class]="buttonClasses"
[disabled]="disabled"
[type]="type"
(click)="onClick($event)">
<lib-icon *ngIf="icon" [name]="icon" [position]="iconPosition"></lib-icon>
<span class="button-text">
<ng-content></ng-content>
</span>
<lib-spinner *ngIf="loading" size="small"></lib-spinner>
</button>
`,
styleUrls: ['./button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'danger' | 'ghost' = 'primary';
@Input() size: 'small' | 'medium' | 'large' = 'medium';
@Input() disabled = false;
@Input() loading = false;
@Input() icon?: string;
@Input() iconPosition: 'left' | 'right' = 'left';
@Input() type: 'button' | 'submit' | 'reset' = 'button';
@Input() fullWidth = false;
@Output() clicked = new EventEmitter<MouseEvent>();
get buttonClasses(): string {
return [
'lib-button',
`lib-button--${this.variant}`,
`lib-button--${this.size}`,
this.fullWidth ? 'lib-button--full-width' : '',
this.loading ? 'lib-button--loading' : '',
this.disabled ? 'lib-button--disabled' : ''
].filter(Boolean).join(' ');
}
onClick(event: MouseEvent): void {
if (!this.disabled && !this.loading) {
this.clicked.emit(event);
}
}
}
// Form Field Component
@Component({
selector: 'lib-form-field',
template: `
<div class="form-field" [class.has-error]="hasError">
<label *ngIf="label" [for]="fieldId" class="form-label">
{{ label }}
<span *ngIf="required" class="required-indicator">*</span>
</label>
<div class="form-control-wrapper">
<ng-content></ng-content>
<lib-icon
*ngIf="hasError"
name="error"
class="error-icon">
</lib-icon>
</div>
<div *ngIf="hint && !hasError" class="form-hint">
{{ hint }}
</div>
<div *ngIf="hasError" class="form-error">
<ng-container *ngFor="let error of errors">
{{ error }}
</ng-container>
</div>
</div>
`,
styleUrls: ['./form-field.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}
]
})
export class FormFieldComponent implements OnInit, AfterContentInit {
@Input() label?: string;
@Input() hint?: string;
@Input() required = false;
@Input() errors: string[] = [];
@ContentChild(NgControl) control?: NgControl;
fieldId = `field-${Math.random().toString(36).substr(2, 9)}`;
get hasError(): boolean {
return this.errors.length > 0 ||
(this.control?.invalid && this.control?.touched) || false;
}
ngOnInit(): void {
// Component initialization
}
ngAfterContentInit(): void {
if (this.control) {
// Set up control integration
}
}
}
Advanced Lifecycle Management¶
Custom Lifecycle Hooks¶
// Custom Lifecycle Interface
export interface OnVisible {
onVisible(): void;
}
export interface OnHidden {
onHidden(): void;
}
// Visibility Directive
@Directive({
selector: '[appVisibility]'
})
export class VisibilityDirective implements OnInit, OnDestroy {
@Output() visibilityChange = new EventEmitter<boolean>();
private observer?: IntersectionObserver;
constructor(private element: ElementRef) {}
ngOnInit(): void {
this.setupIntersectionObserver();
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
private setupIntersectionObserver(): void {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const isVisible = entry.isIntersecting;
this.visibilityChange.emit(isVisible);
// Call lifecycle methods if they exist
const component = this.getComponentInstance();
if (component) {
if (isVisible && 'onVisible' in component) {
(component as OnVisible).onVisible();
} else if (!isVisible && 'onHidden' in component) {
(component as OnHidden).onHidden();
}
}
});
},
{ threshold: 0.1 }
);
this.observer.observe(this.element.nativeElement);
}
private getComponentInstance(): any {
// Implementation to get component instance
return null;
}
}
// Component using custom lifecycle
@Component({
selector: 'app-lazy-content',
template: `
<div appVisibility (visibilityChange)="onVisibilityChange($event)">
<div *ngIf="isVisible">
<app-expensive-component></app-expensive-component>
</div>
</div>
`
})
export class LazyContentComponent implements OnVisible, OnHidden {
isVisible = false;
onVisible(): void {
console.log('Component became visible');
this.loadContent();
}
onHidden(): void {
console.log('Component became hidden');
this.cleanupResources();
}
onVisibilityChange(visible: boolean): void {
this.isVisible = visible;
}
private loadContent(): void {
// Load heavy content when visible
}
private cleanupResources(): void {
// Cleanup when hidden
}
}
Performance Optimization Techniques¶
OnPush Change Detection Strategy¶
@Component({
selector: 'app-optimized-list',
template: `
<div class="list-container">
<div
*ngFor="let item of items; trackBy: trackByFn"
class="list-item"
[class.selected]="item.id === selectedId">
<app-list-item
[item]="item"
(selected)="onItemSelected($event)">
</app-list-item>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedListComponent {
@Input() items: Item[] = [];
@Input() selectedId?: number;
@Output() itemSelected = new EventEmitter<Item>();
constructor(private cdr: ChangeDetectorRef) {}
trackByFn(index: number, item: Item): number {
return item.id;
}
onItemSelected(item: Item): void {
this.itemSelected.emit(item);
}
// Manual change detection trigger when needed
refreshView(): void {
this.cdr.markForCheck();
}
}
Async Pipe Optimization¶
@Component({
selector: 'app-async-optimized',
template: `
<div *ngIf="data$ | async as data">
<div class="header">{{ data.title }}</div>
<div class="content">{{ data.description }}</div>
<div class="items">
<div *ngFor="let item of data.items">
{{ item.name }}
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AsyncOptimizedComponent {
data$ = this.dataService.getData().pipe(
shareReplay(1), // Cache the result
startWith(null), // Provide initial value
catchError(error => {
console.error('Data loading error:', error);
return of(null);
})
);
constructor(private dataService: DataService) {}
}
Best Practices¶
Component Design Principles¶
- Single Responsibility: Each component should have one clear purpose
- Composition over Inheritance: Prefer component composition
- Input/Output Principle: Use @Input for data flow down, @Output for events up
- Immutability: Always treat inputs as immutable
- Pure Components: Minimize side effects in components
Component Communication Guidelines¶
// Service for sibling component communication
@Injectable({
providedIn: 'root'
})
export class ComponentCommunicationService {
private messageSubject = new Subject<ComponentMessage>();
public messages$ = this.messageSubject.asObservable();
sendMessage(message: ComponentMessage): void {
this.messageSubject.next(message);
}
}
interface ComponentMessage {
type: string;
payload: any;
sourceComponent: string;
}
// Example usage
@Component({
selector: 'app-sender',
template: `<button (click)="sendMessage()">Send Message</button>`
})
export class SenderComponent {
constructor(private communicationService: ComponentCommunicationService) {}
sendMessage(): void {
this.communicationService.sendMessage({
type: 'USER_SELECTED',
payload: { userId: 123 },
sourceComponent: 'SenderComponent'
});
}
}
@Component({
selector: 'app-receiver',
template: `<div>{{ lastMessage | json }}</div>`
})
export class ReceiverComponent implements OnInit, OnDestroy {
lastMessage: ComponentMessage | null = null;
private subscription?: Subscription;
constructor(private communicationService: ComponentCommunicationService) {}
ngOnInit(): void {
this.subscription = this.communicationService.messages$
.pipe(
filter(message => message.type === 'USER_SELECTED')
)
.subscribe(message => {
this.lastMessage = message;
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
Practical Exercises¶
Exercise 1: Build a Reusable Modal System¶
Create a modal system with the following features:
- Multiple modal types (confirmation, form, custom)
- Configurable size and behavior
- Service-based modal management
- Animation support
Requirements:
- Create a
ModalServicefor programmatic modal control - Implement different modal types using content projection
- Add keyboard and overlay interaction handling
- Implement modal stacking support
Exercise 2: Dynamic Dashboard Components¶
Build a dashboard system where components can be dynamically loaded and arranged:
Requirements:
- Create a widget registry system
- Implement drag-and-drop widget arrangement
- Add widget configuration capabilities
- Persist dashboard layout
Exercise 3: Advanced Form Component Library¶
Create a comprehensive form component library:
Requirements:
- Multiple input types with consistent styling
- Validation integration
- Accessibility features
- Theme support
Summary¶
In this module, you learned advanced component patterns and architectural concepts:
- Component Patterns: Container-Presenter, Composition patterns
- Content Projection: Single and multi-slot projection with conditional rendering
- Dynamic Components: Component factories, portals, and runtime component creation
- Component Libraries: Reusable component design and implementation
- Performance: OnPush strategy, async optimization, and change detection control
- Architecture: Communication patterns and best practices
These advanced techniques enable you to build scalable, maintainable, and high-performance Angular applications with sophisticated component architectures.
Next Steps¶
- Explore Angular Elements for creating custom elements
- Learn about Micro Frontends with Angular
- Study advanced testing patterns for complex components
- Investigate Angular Universal for server-side rendering
- Practice with Angular CDK for advanced UI components