This content originally appeared on DEV Community and was authored by Blueprintblog
The complete practical guide to Angular’s most significant updates since its inception
Angular 17+ didn’t just add new features—it fundamentally rewrote the rules of modern frontend development.
Most developers are still using outdated patterns like *ngIf
, *ngFor
, and complex NgModules, missing out on the 87% build performance improvements and dramatically simplified syntax that Angular 17+ brings to the table.
The truth is: Angular 17, 18, 19, and 20 represent the most significant paradigm shift in Angular’s history, introducing standalone-first architecture, revolutionary control flow syntax, and signal-based reactivity that makes development faster, more intuitive, and significantly more performant.
In this guide, you’ll master the five core concepts that define modern Angular development: New Control Flow Syntax, Standalone Components, Angular Signals, Deferrable Views, and the New Build System—plus learn the exact migration strategies to modernize your existing applications.
Concept #1: New Control Flow Syntax
The Concept:
Angular 17 introduced a revolutionary template syntax that replaces structural directives (*ngIf
, *ngFor
, *ngSwitch
) with JavaScript-like control flow blocks (@if
, @for
, @switch
) that are built into the framework core.
Why This Matters:
This new syntax enables more ergonomic and much less verbose code that is closer to JavaScript, requiring fewer documentation lookups, better type checking thanks to optimal type narrowing, and reduced runtime footprint that helps drop bundle size and improve Core Web Vitals.
How It Works:
The control flow blocks are processed at build-time rather than runtime, eliminating the need for importing directives and providing superior performance characteristics.
Implementation:
// ==========================================
// 🎬 NEW CONTROL FLOW: Modern Angular Templates
// ==========================================
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<!-- ✅ @if - Conditional Rendering -->
@if (user.isAuthenticated) {
<div class="welcome-panel">
<h2>Welcome back, {{user.name}}!</h2>
@if (user.hasNotifications; as notifications) {
<div class="notification-badge">
{{notifications.length}} new messages
</div>
}
</div>
} @else {
<div class="login-prompt">
<p>Please log in to continue</p>
<button (click)="showLogin()">Sign In</button>
</div>
}
<!-- ✅ @for - List Iteration with Performance Tracking -->
<div class="projects-grid">
@for (project of projects; track project.id; let idx = $index) {
<div class="project-card"
[class.featured]="idx < 3">
<h3>{{project.title}}</h3>
<p>{{project.description}}</p>
<span class="status">{{project.status}}</span>
</div>
} @empty {
<div class="empty-state">
<p>No projects found</p>
<button (click)="createProject()">Create Your First Project</button>
</div>
}
</div>
<!-- ✅ @switch - Multiple Condition Handling -->
@switch (user.role) {
@case ('admin') {
<admin-dashboard [user]="user" />
}
@case ('manager') {
<manager-dashboard [projects]="userProjects" />
}
@case ('developer') {
<developer-dashboard [tasks]="activeTasks" />
}
@default {
<guest-dashboard />
}
}
`
})
export class DashboardComponent {
user = signal({
isAuthenticated: true,
name: 'John Doe',
role: 'developer',
hasNotifications: [
{ id: 1, message: 'Project updated' },
{ id: 2, message: 'New task assigned' }
]
});
projects = signal([
{ id: 1, title: 'E-commerce Platform', description: 'React-based shopping app', status: 'active' },
{ id: 2, title: 'Analytics Dashboard', description: 'Data visualization tool', status: 'completed' }
]);
}
Migration from Legacy Syntax:
# Automatic migration command
ng generate @angular/core:control-flow
// ❌ OLD: Structural Directives
@Component({
template: `
<div *ngIf="user.isAuthenticated; else loginBlock">
<h2>Welcome, {{user.name}}!</h2>
</div>
<ng-template #loginBlock>
<p>Please log in</p>
</ng-template>
<div *ngFor="let project of projects; trackBy: trackByFn; let i = index">
{{project.title}}
</div>
<div [ngSwitch]="user.role">
<admin-dashboard *ngSwitchCase="'admin'"></admin-dashboard>
<user-dashboard *ngSwitchDefault></user-dashboard>
</div>
`,
imports: [CommonModule] // Required import
})
// ✅ NEW: Built-in Control Flow
@Component({
template: `
@if (user.isAuthenticated) {
<h2>Welcome, {{user.name}}!</h2>
} @else {
<p>Please log in</p>
}
@for (project of projects; track project.id; let i = $index) {
{{project.title}}
}
@switch (user.role) {
@case ('admin') { <admin-dashboard /> }
@default { <user-dashboard /> }
}
`
// No imports needed!
})
Concept #2: Standalone Components by Default
The Concept:
Starting with Angular 19, all components, directives, and pipes are standalone by default, eliminating the need to explicitly set standalone: true
and removing dependency on NgModules for most use cases.
Why This Matters:
Standalone components drastically reduce boilerplate code, improve tree-shaking, enable better lazy loading, and create a more intuitive development experience that aligns with modern component-based architecture patterns.
How It Works:
Standalone components can directly import what they need, eliminating the complex module dependency chains and making components more self-contained and reusable.
Implementation:
// ==========================================
// 🎯 STANDALONE COMPONENTS: Modern Architecture
// ==========================================
// ✅ Modern Standalone Component (Angular 19+)
@Component({
selector: 'app-user-profile',
standalone: true, // Default in Angular 19+
imports: [
// Direct imports - no modules needed
ReactiveFormsModule,
JsonPipe,
DatePipe,
RouterLink,
// Other standalone components
UserAvatarComponent,
NotificationBellComponent
],
template: `
<div class="profile-container">
<app-user-avatar
[imageUrl]="userForm.value.avatar"
[size]="'large'"
(click)="editAvatar()" />
<form [formGroup]="userForm" (ngSubmit)="saveProfile()">
@if (isEditing) {
<div class="form-fields">
<input
formControlName="name"
placeholder="Full name"
[class.error]="userForm.get('name')?.invalid && userForm.get('name')?.touched" />
<input
formControlName="email"
type="email"
placeholder="Email address" />
<div class="form-actions">
<button type="submit" [disabled]="userForm.invalid">
Save Changes
</button>
<button type="button" (click)="cancelEdit()">
Cancel
</button>
</div>
</div>
} @else {
<div class="profile-display">
<h2>{{userForm.value.name}}</h2>
<p>{{userForm.value.email}}</p>
<button (click)="startEdit()">Edit Profile</button>
</div>
}
</form>
<app-notification-bell
[notifications]="notifications()"
(notificationClick)="handleNotification($event)" />
</div>
`,
styles: [`
.profile-container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.form-fields input.error {
border-color: #e74c3c;
background-color: #fdf2f2;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
`]
})
export class UserProfileComponent {
// Signal-based state management
notifications = signal([
{ id: 1, message: 'Profile updated successfully', type: 'success' },
{ id: 2, message: 'New message received', type: 'info' }
]);
isEditing = signal(false);
userForm = this.fb.group({
name: ['John Doe', [Validators.required, Validators.minLength(2)]],
email: ['john@example.com', [Validators.required, Validators.email]],
avatar: ['https://api.dicebear.com/7.x/avataaars/svg?seed=John']
});
constructor(private fb: FormBuilder) {}
startEdit() {
this.isEditing.set(true);
}
cancelEdit() {
this.isEditing.set(false);
// Reset form to original values
this.userForm.reset();
}
saveProfile() {
if (this.userForm.valid) {
// Save logic here
console.log('Saving profile:', this.userForm.value);
this.isEditing.set(false);
// Add success notification
this.notifications.update(current => [...current, {
id: Date.now(),
message: 'Profile updated successfully!',
type: 'success'
}]);
}
}
}
// ✅ Reusable Standalone Child Component
@Component({
selector: 'app-user-avatar',
standalone: true,
template: `
<div class="avatar-container" [style.width.px]="avatarSize" [style.height.px]="avatarSize">
<img
[src]="imageUrl"
[alt]="'User avatar'"
(error)="onImageError()"
[style.width.px]="avatarSize"
[style.height.px]="avatarSize" />
@if (isEditable) {
<div class="edit-overlay">
<span>✏</span>
</div>
}
</div>
`,
styles: [`
.avatar-container {
position: relative;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease;
}
.avatar-container:hover {
transform: scale(1.05);
}
.edit-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.avatar-container:hover .edit-overlay {
opacity: 1;
}
`]
})
export class UserAvatarComponent {
@Input() imageUrl: string = '';
@Input() size: 'small' | 'medium' | 'large' = 'medium';
@Input() isEditable: boolean = true;
get avatarSize(): number {
const sizes = { small: 40, medium: 60, large: 120 };
return sizes[this.size];
}
onImageError() {
// Fallback to default avatar
this.imageUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=default';
}
}
Bootstrapping Standalone Applications:
// ==========================================
// 🚀 BOOTSTRAP: Standalone Application Setup
// ==========================================
// main.ts - Modern Bootstrap
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
// Add other providers as needed
]
}).catch(err => console.error(err));
// app.component.ts - Root Standalone Component
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeaderComponent, FooterComponent],
template: `
<app-header />
<main class="main-content">
<router-outlet />
</main>
<app-footer />
`
})
export class AppComponent {
title = 'Modern Angular App';
}
// app.routes.ts - Route Configuration
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'profile',
loadComponent: () => import('./profile/user-profile.component').then(m => m.UserProfileComponent)
},
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.dashboardRoutes)
}
];
Concept #3: Angular Signals – Reactive State Management
The Concept:
Angular Signals are reactive primitives that represent values and allow controlled changes with automatic change tracking, graduated to stable in Angular 20, enabling fine-grained DOM updates and eliminating the need for Zone.js.
Why This Matters:
With Signals, Angular can determine exactly what parts of the page need to be updated and update only those, in contrast to default change detection where Angular checks all components on the page even if their data didn’t change.
How It Works:
Signals create a reactive graph where computed values automatically update when their dependencies change, enabling predictable state management and optimal rendering performance.
Implementation:
// ==========================================
// ⚡ ANGULAR SIGNALS: Reactive State Management
// ==========================================
@Component({
selector: 'app-shopping-cart',
standalone: true,
imports: [CurrencyPipe, DecimalPipe],
template: `
<div class="cart-container">
<h2>Shopping Cart ({{cartItems().length}} items)</h2>
@if (cartItems().length === 0) {
<div class="empty-cart">
<p>Your cart is empty</p>
<button (click)="addSampleItems()">Add Sample Items</button>
</div>
} @else {
<div class="cart-items">
@for (item of cartItems(); track item.id) {
<div class="cart-item">
<div class="item-info">
<h4>{{item.name}}</h4>
<p class="price">{{item.price | currency}}</p>
</div>
<div class="quantity-controls">
<button
(click)="updateQuantity(item.id, item.quantity - 1)"
[disabled]="item.quantity <= 1">-</button>
<span>{{item.quantity}}</span>
<button
(click)="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
<div class="item-total">
{{item.price * item.quantity | currency}}
</div>
<button
class="remove-btn"
(click)="removeItem(item.id)">Remove</button>
</div>
}
</div>
<div class="cart-summary">
<div class="summary-row">
<span>Subtotal:</span>
<span>{{subtotal() | currency}}</span>
</div>
<div class="summary-row">
<span>Tax ({{TAX_RATE | percent}}):</span>
<span>{{taxAmount() | currency}}</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span>{{total() | currency}}</span>
</div>
<button
class="checkout-btn"
[disabled]="isProcessing()"
(click)="processCheckout()">
@if (isProcessing()) {
Processing...
} @else {
Checkout ({{total() | currency}})
}
</button>
</div>
}
</div>
`,
styles: [`
.cart-container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.cart-item {
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 1rem;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.quantity-controls { display: flex; align-items: center; gap: 0.5rem; }
.cart-summary {
margin-top: 2rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.total { font-weight: bold; font-size: 1.2em; border-top: 1px solid #ddd; padding-top: 0.5rem; }
.checkout-btn {
width: 100%;
padding: 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1.1em;
margin-top: 1rem;
}
`]
})
export class ShoppingCartComponent {
readonly TAX_RATE = 0.08;
// ✅ Signal-based reactive state
cartItems = signal<CartItem[]>([]);
isProcessing = signal(false);
// ✅ Computed signals - automatically update when dependencies change
subtotal = computed(() =>
this.cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
taxAmount = computed(() =>
this.subtotal() * this.TAX_RATE
);
total = computed(() =>
this.subtotal() + this.taxAmount()
);
// ✅ Effect - runs when signals change
constructor() {
// Automatically save cart to localStorage when it changes
effect(() => {
const items = this.cartItems();
localStorage.setItem('cart', JSON.stringify(items));
console.log(`Cart updated: ${items.length} items, total: $${this.total().toFixed(2)}`);
});
// Load cart from localStorage on init
this.loadCartFromStorage();
}
updateQuantity(itemId: number, newQuantity: number) {
if (newQuantity < 1) return;
this.cartItems.update(items =>
items.map(item =>
item.id === itemId
? { ...item, quantity: newQuantity }
: item
)
);
}
removeItem(itemId: number) {
this.cartItems.update(items =>
items.filter(item => item.id !== itemId)
);
}
addSampleItems() {
const sampleItems: CartItem[] = [
{ id: 1, name: 'Wireless Headphones', price: 99.99, quantity: 1 },
{ id: 2, name: 'Bluetooth Speaker', price: 79.99, quantity: 2 },
{ id: 3, name: 'Phone Case', price: 24.99, quantity: 1 }
];
this.cartItems.set(sampleItems);
}
async processCheckout() {
this.isProcessing.set(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
alert(`Order processed successfully! Total: ${this.total().toFixed(2)}`);
this.cartItems.set([]);
} catch (error) {
alert('Checkout failed. Please try again.');
} finally {
this.isProcessing.set(false);
}
}
private loadCartFromStorage() {
const saved = localStorage.getItem('cart');
if (saved) {
try {
const items = JSON.parse(saved) as CartItem[];
this.cartItems.set(items);
} catch (error) {
console.error('Failed to load cart from storage:', error);
}
}
}
}
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
Advanced Signal Patterns:
// ==========================================
// 🔧 ADVANCED SIGNALS: Patterns & Best Practices
// ==========================================
// ✅ Signal-based Service
@Injectable({ providedIn: 'root' })
export class UserService {
private userState = signal<User | null>(null);
private loading = signal(false);
private error = signal<string | null>(null);
// Public readonly signals
readonly user = this.userState.asReadonly();
readonly isLoading = this.loading.asReadonly();
readonly error$ = this.error.asReadonly();
// Computed user properties
readonly isAuthenticated = computed(() => !!this.userState());
readonly userDisplayName = computed(() => {
const user = this.userState();
return user ? `${user.firstName} ${user.lastName}` : 'Guest';
});
// linkedSignal for derived state
readonly preferences = linkedSignal(() => {
const user = this.userState();
return user?.preferences ?? getDefaultPreferences();
});
async loadUser(userId: string) {
this.loading.set(true);
this.error.set(null);
try {
const user = await this.http.get<User>(`/api/users/${userId}`).toPromise();
this.userState.set(user);
} catch (err) {
this.error.set('Failed to load user');
console.error('User loading error:', err);
} finally {
this.loading.set(false);
}
}
updateUserPreferences(preferences: UserPreferences) {
this.userState.update(user =>
user ? { ...user, preferences } : null
);
}
logout() {
this.userState.set(null);
this.error.set(null);
}
}
// ✅ Signal integration with RxJS
@Component({
selector: 'app-live-data',
template: `
<div class="live-dashboard">
<h3>Live Data Dashboard</h3>
@if (connectionStatus() === 'connected') {
<div class="status connected">🟢 Connected</div>
} @else {
<div class="status disconnected">🔴 Disconnected</div>
}
<div class="metrics">
<div class="metric">
<label>Current Value:</label>
<span>{{currentValue()}}</span>
</div>
<div class="metric">
<label>Average:</label>
<span>{{averageValue() | number:'1.2-2'}}</span>
</div>
<div class="metric">
<label>Updates:</label>
<span>{{updateCount()}}</span>
</div>
</div>
</div>
`
})
export class LiveDataComponent implements OnDestroy {
// Convert Observable to Signal
currentValue = toSignal(
interval(1000).pipe(
map(() => Math.floor(Math.random() * 100))
),
{ initialValue: 0 }
);
// Convert Signal to Observable for RxJS operations
currentValue$ = toObservable(this.currentValue);
// Advanced computed with RxJS integration
averageValue = toSignal(
this.currentValue$.pipe(
scan((acc, current) => [...acc.slice(-9), current], [] as number[]),
map(values => values.reduce((sum, val) => sum + val, 0) / values.length)
),
{ initialValue: 0 }
);
updateCount = signal(0);
connectionStatus = signal<'connected' | 'disconnected'>('connected');
private subscription = new Subscription();
constructor() {
// Effect to track updates
effect(() => {
this.currentValue(); // Read the signal to create dependency
this.updateCount.update(count => count + 1);
});
// Simulate connection status changes
this.subscription.add(
interval(10000).subscribe(() => {
this.connectionStatus.update(status =>
status === 'connected' ? 'disconnected' : 'connected'
);
})
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Concept #4: Deferrable Views & Performance Optimization
The Concept:
Deferrable views provide an ergonomic API for deferred code loading, allowing developers to lazy load parts of templates based on specific triggers, graduating to stable in Angular 18.
Why This Matters:
Deferrable views dramatically improve initial page load performance by splitting large applications into smaller chunks that load on-demand, reducing Time to Interactive (TTI) and improving Core Web Vitals.
How It Works:
The @defer
block allows you to declaratively specify when parts of your template should be loaded, with built-in triggers for viewport intersection, interaction, timers, and custom conditions.
Implementation:
// ==========================================
// 🎬 DEFERRABLE VIEWS: Performance Optimization
// ==========================================
@Component({
selector: 'app-product-page',
standalone: true,
template: `
<div class="product-page">
<!-- ✅ Critical content loads immediately -->
<header class="product-header">
<h1>{{product.name}}</h1>
<div class="price">{{product.price | currency}}</div>
<button class="buy-now" (click)="addToCart()">
Add to Cart
</button>
</header>
<!-- ✅ Defer reviews until user scrolls down -->
@defer (on viewport; prefetch on idle) {
<app-product-reviews
[productId]="product.id"
[averageRating]="product.rating" />
} @placeholder {
<div class="reviews-skeleton">
<div class="skeleton-lines">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
} @loading (minimum 500ms) {
<div class="loading-reviews">
<div class="spinner"></div>
<p>Loading reviews...</p>
</div>
} @error {
<div class="error-state">
<p>Failed to load reviews</p>
<button (click)="retryLoadReviews()">Retry</button>
</div>
}
<!-- ✅ Defer recommendations until user interacts -->
@defer (on interaction(viewRecommendations); prefetch on hover(viewRecommendations)) {
<app-product-recommendations
[categoryId]="product.categoryId"
[excludeProductId]="product.id" />
} @placeholder {
<button
#viewRecommendations
class="view-recommendations">
View Recommendations
</button>
}
<!-- ✅ Defer heavy analytics component until after 3 seconds -->
@defer (on timer(3s)) {
<app-analytics-tracker
[productId]="product.id"
[userId]="currentUser?.id" />
}
<!-- ✅ Conditional deferring based on user preferences -->
@defer (when shouldLoadAdvancedFeatures(); prefetch when isPreloadEnabled()) {
<app-advanced-product-tools
[product]="product"
[userTier]="currentUser?.tier" />
} @placeholder {
@if (currentUser?.tier === 'premium') {
<div class="upgrade-prompt">
<p>Advanced tools available for premium users</p>
<button (click)="enableAdvancedFeatures()">Enable Advanced Tools</button>
</div>
}
}
</div>
`,
styles: [`
.product-page { max-width: 1200px; margin: 0 auto; padding: 2rem; }
.product-header { margin-bottom: 3rem; }
.reviews-skeleton, .loading-reviews {
padding: 2rem;
border: 1px solid #eee;
border-radius: 8px;
margin: 2rem 0;
}
.skeleton-lines { display: flex; flex-direction: column; gap: 1rem; }
.skeleton-line {
height: 1rem;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
border-radius: 4px;
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
.skeleton-line.short { width: 60%; }
@keyframes skeleton-pulse {
0% { background-position: -200px 0; }
100% { background-position: calc(200px + 100%) 0; }
}
.spinner {
width: 2rem;
height: 2rem;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`]
})
export class ProductPageComponent {
product = {
id: 1,
name: 'Premium Wireless Headphones',
price: 299.99,
rating: 4.5,
categoryId: 'electronics'
};
currentUser = signal<User | null>(null);
advancedFeaturesEnabled = signal(false);
preloadEnabled = signal(true);
shouldLoadAdvancedFeatures = computed(() =>
this.currentUser()?.tier === 'premium' && this.advancedFeaturesEnabled()
);
isPreloadEnabled = computed(() => this.preloadEnabled());
enableAdvancedFeatures() {
this.advancedFeaturesEnabled.set(true);
}
addToCart() {
console.log('Added to cart:', this.product.name);
}
retryLoadReviews() {
// Trigger re-evaluation of defer block
console.log('Retrying reviews load...');
}
}
// ✅ Heavy component that benefits from deferring
@Component({
selector: 'app-product-reviews',
standalone: true,
imports: [DatePipe, DecimalPipe],
template: `
<div class="reviews-section">
<h3>Customer Reviews ({{reviews.length}})</h3>
<div class="rating-summary">
<div class="average-rating">
<span class="rating-number">{{averageRating}}</span>
<div class="stars">
@for (star of starArray; track $index) {
<span [class]="getStarClass($index)">⭐</span>
}
</div>
</div>
</div>
<div class="reviews-list">
@for (review of reviews; track review.id) {
<div class="review-item">
<div class="review-header">
<strong>{{review.author}}</strong>
<span class="review-date">{{review.date | date}}</span>
<div class="review-rating">
Rating: {{review.rating}}/5
</div>
</div>
<p class="review-content">{{review.content}}</p>
</div>
} @empty {
<p>No reviews yet. Be the first to review!</p>
}
</div>
</div>
`,
styles: [`
.reviews-section { margin: 2rem 0; padding: 2rem; border: 1px solid #ddd; border-radius: 8px; }
.rating-summary { margin-bottom: 2rem; text-align: center; }
.average-rating { display: flex; align-items: center; justify-content: center; gap: 1rem; }
.rating-number { font-size: 2rem; font-weight: bold; }
.review-item { padding: 1rem; border-bottom: 1px solid #eee; }
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.review-content { color: #666; line-height: 1.6; }
`]
})
export class ProductReviewsComponent implements OnInit {
@Input() productId!: number;
@Input() averageRating!: number;
reviews: Review[] = [];
starArray = Array(5).fill(0);
async ngOnInit() {
// Simulate API call with delay
await new Promise(resolve => setTimeout(resolve, 1000));
this.reviews = [
{
id: 1,
author: 'John D.',
rating: 5,
content: 'Excellent sound quality and comfortable fit. Highly recommended!',
date: new Date('2024-01-15')
},
{
id: 2,
author: 'Sarah M.',
rating: 4,
content: 'Great headphones, battery life could be better.',
date: new Date('2024-01-10')
}
];
}
getStarClass(index: number): string {
return index < Math.floor(this.averageRating) ? 'filled' : 'empty';
}
}
interface Review {
id: number;
author: string;
rating: number;
content: string;
date: Date;
}
Concept #5: New Build System & Developer Experience
The Concept:
Angular 17+ introduced a new esbuild and Vite-based application builder that replaced Webpack, providing up to 87% build time improvements and reducing Angular CLI dependencies by 50%.
Why This Matters:
The new build system dramatically improves development speed with faster hot module replacement, smaller bundle sizes, and better tree-shaking, while providing a more modern development experience.
How It Works:
The new builder uses esbuild for production builds and Vite for development server, providing near-instantaneous reloads and optimized bundle generation.
Implementation:
// ==========================================
// 🏗 NEW BUILD SYSTEM: Configuration & Setup
// ==========================================
// angular.json - Modern Build Configuration
{
"projects": {
"my-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": [],
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
}
],
"outputHashing": "all",
"optimization": true,
"sourceMap": false
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "my-app:build:production"
},
"development": {
"buildTarget": "my-app:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}
Performance Optimization Strategies:
// ==========================================
// ⚡ PERFORMANCE: Modern Angular Optimization
// ==========================================
// 1. ✅ Lazy Loading with Standalone Components
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'products',
loadChildren: () => import('./products/product.routes').then(m => m.productRoutes)
},
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
canActivate: [authGuard]
}
];
// 2. ✅ Optimized Imports with Tree Shaking
// Bad: Imports entire library
// import * as _ from 'lodash';
// Good: Import only what you need
import { debounce, throttle } from 'lodash-es';
// 3. ✅ OnPush Strategy with Signals
@Component({
selector: 'app-optimized-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (item of filteredItems(); track item.id) {
<app-list-item
[item]="item"
(itemClick)="onItemClick($event)" />
}
`
})
export class OptimizedListComponent {
items = signal<Item[]>([]);
searchTerm = signal('');
// Computed signal automatically memoizes results
filteredItems = computed(() => {
const search = this.searchTerm().toLowerCase();
return this.items().filter(item =>
item.name.toLowerCase().includes(search)
);
});
onItemClick(item: Item) {
// Handle click with signals
console.log('Clicked:', item.name);
}
}
// 4. ✅ Service Worker for Caching
// ngsw-config.json
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
],
"dataGroups": [
{
"name": "api-cache",
"urls": ["/api/**"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 100,
"maxAge": "1h"
}
}
]
}
Development Experience Enhancements:
// ==========================================
// 🔧 DEV EXPERIENCE: Modern Angular Tooling
// ==========================================
// 1. ✅ TypeScript 5.4+ Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"importHelpers": true,
"skipLibCheck": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true,
"strictStandalone": true
}
}
// 2. ✅ Hot Module Replacement Setup
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
if (import.meta.hot) {
import.meta.hot.accept();
}
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
// Other providers...
]
});
// 3. ✅ Environment Configuration
// environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
features: {
enableAdvancedFeatures: true,
enableAnalytics: false
}
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com',
features: {
enableAdvancedFeatures: true,
enableAnalytics: true
}
};
// 4. ✅ Modern Testing Setup
// component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent] // Standalone component
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should update signal reactively', () => {
const testSignal = signal(0);
// Test signal reactivity
expect(testSignal()).toBe(0);
testSignal.set(5);
expect(testSignal()).toBe(5);
testSignal.update(value => value * 2);
expect(testSignal()).toBe(10);
});
});
Integration Strategy: Building Modern Angular Applications
Your Progressive Migration Approach:
Phase 1: Foundation Setup
-
Update to Angular 17+ using
ng update @angular/core@latest @angular/cli@latest
- Enable new build system in angular.json (automatic for new projects)
- Convert to standalone bootstrap in main.ts
Phase 2: Template Modernization
-
Migrate control flow with
ng generate @angular/core:control-flow
- Convert components to standalone gradually, starting with leaf components
- Implement deferrable views for performance-critical sections
Phase 3: Reactivity Enhancement
- Introduce signals for state management, replacing services where appropriate
- Convert to signal-based forms when available (Angular 21+)
- Optimize with computed signals and effects for reactive patterns
Production-Ready Architecture Pattern:
// ==========================================
// 🎯 COMPLETE INTEGRATION: Modern Angular App
// ==========================================
// Feature-based standalone architecture
@Component({
selector: 'app-modern-feature',
standalone: true,
imports: [
// Angular modules
ReactiveFormsModule,
CommonModule,
// Standalone components
HeaderComponent,
LoaderComponent,
ErrorBoundaryComponent
],
template: `
<div class="feature-container">
<app-header [title]="featureTitle()" />
@defer (on viewport; prefetch on idle) {
<div class="main-content">
@if (loading()) {
<app-loader message="Loading data..." />
} @else if (error()) {
<app-error-boundary
[error]="error()"
(retry)="loadData()" />
} @else {
<div class="data-grid">
@for (item of filteredData(); track item.id) {
<div class="data-item"
[class.selected]="selectedIds().includes(item.id)"
(click)="toggleSelection(item.id)">
<h3>{{item.title}}</h3>
<p>{{item.description}}</p>
@if (item.status === 'active') {
<span class="badge active">Active</span>
} @else {
<span class="badge inactive">Inactive</span>
}
</div>
} @empty {
<div class="empty-state">
<p>No data available</p>
<button (click)="loadSampleData()">Load Sample Data</button>
</div>
}
</div>
}
</div>
} @placeholder {
<div class="content-placeholder">
<div class="skeleton-grid">
@for (placeholder of placeholderArray; track $index) {
<div class="skeleton-item"></div>
}
</div>
</div>
}
@if (selectedIds().length > 0) {
<div class="action-bar">
<span>{{selectedIds().length}} items selected</span>
<button (click)="bulkAction('delete')">Delete Selected</button>
<button (click)="bulkAction('archive')">Archive Selected</button>
</div>
}
</div>
`,
styles: [`
.feature-container { min-height: 100vh; padding: 1rem; }
.data-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.data-item {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.data-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.data-item.selected { border-color: #007bff; background-color: #f8f9ff; }
.badge { padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.75rem; }
.badge.active { background: #d4edda; color: #155724; }
.badge.inactive { background: #f8d7da; color: #721c24; }
.skeleton-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.skeleton-item {
height: 150px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
border-radius: 8px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.action-bar {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: #fff;
padding: 1rem 2rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 1rem;
}
`]
})
export class ModernFeatureComponent implements OnInit {
// Signal-based reactive state
private dataState = signal<DataItem[]>([]);
private loadingState = signal(false);
private errorState = signal<string | null>(null);
private selectedIdsState = signal<number[]>([]);
private searchTerm = signal('');
// Public readonly signals
readonly data = this.dataState.asReadonly();
readonly loading = this.loadingState.asReadonly();
readonly error = this.errorState.asReadonly();
readonly selectedIds = this.selectedIdsState.asReadonly();
// Computed derived state
readonly filteredData = computed(() => {
const search = this.searchTerm().toLowerCase();
return this.dataState().filter(item =>
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search)
);
});
readonly featureTitle = computed(() =>
`Data Management (${this.filteredData().length} items)`
);
readonly placeholderArray = Array(6).fill(null);
constructor(
private dataService: DataService,
private notificationService: NotificationService
) {
// Effect for auto-save
effect(() => {
const selected = this.selectedIds();
if (selected.length > 0) {
sessionStorage.setItem('selectedItems', JSON.stringify(selected));
}
});
}
async ngOnInit() {
await this.loadData();
this.restoreSelection();
}
async loadData() {
this.loadingState.set(true);
this.errorState.set(null);
try {
const data = await this.dataService.fetchData();
this.dataState.set(data);
} catch (error) {
this.errorState.set('Failed to load data. Please try again.');
console.error('Data loading error:', error);
} finally {
this.loadingState.set(false);
}
}
toggleSelection(itemId: number) {
this.selectedIdsState.update(selected => {
const index = selected.indexOf(itemId);
return index === -1
? [...selected, itemId]
: selected.filter(id => id !== itemId);
});
}
async bulkAction(action: 'delete' | 'archive') {
const selectedIds = this.selectedIds();
if (selectedIds.length === 0) return;
try {
await this.dataService.bulkAction(action, selectedIds);
// Update local state
this.dataState.update(items =>
items.filter(item => !selectedIds.includes(item.id))
);
this.selectedIdsState.set([]);
this.notificationService.show(`${action} completed successfully`);
} catch (error) {
this.notificationService.show(`Failed to ${action} items`);
}
}
loadSampleData() {
const sampleData: DataItem[] = [
{ id: 1, title: 'Sample Item 1', description: 'This is a sample data item', status: 'active' },
{ id: 2, title: 'Sample Item 2', description: 'Another sample data item', status: 'inactive' }
];
this.dataState.set(sampleData);
}
private restoreSelection() {
const saved = sessionStorage.getItem('selectedItems');
if (saved) {
try {
const selectedIds = JSON.parse(saved) as number[];
this.selectedIdsState.set(selectedIds);
} catch (error) {
console.error('Failed to restore selection:', error);
}
}
}
}
interface DataItem {
id: number;
title: string;
description: string;
status: 'active' | 'inactive';
}
Next Steps: Mastering Modern Angular
Implement Progressively:
- Start with new projects using Angular 19+ and standalone architecture
- Migrate existing apps using the provided migration commands and strategies
- Adopt signals gradually for new features before converting existing services
Then advance to:
- Signal-based forms (coming in Angular 21+)
- Zoneless change detection for maximum performance
- Selectorless components for even cleaner templates
Want to see these concepts in action? Create a new Angular project with ng new --standalone --routing
and start building with the modern patterns covered in this guide.
Related Resources:
Next: Advanced Angular Patterns with Signals and Zoneless Architecture
Follow for more deep-dive Angular tutorials and modern development techniques
This content originally appeared on DEV Community and was authored by Blueprintblog