Angular Offline Support
One of the most powerful features of Progressive Web Applications (PWAs) is their ability to function offline or with poor network connectivity. In this tutorial, we'll explore how to implement offline support in Angular applications using the built-in Angular service worker.
Introduction to Offline Support
Offline support allows your application to:
- Continue functioning when users lose internet connectivity
- Load faster by serving cached resources
- Provide a seamless user experience regardless of network conditions
- Reduce server load and bandwidth consumption
By the end of this tutorial, you'll understand how to implement these capabilities in your Angular applications.
Prerequisites
Before starting, make sure you have:
- Basic knowledge of Angular
- Node.js and npm installed
- Angular CLI installed (
npm install -g @angular/cli
)
Setting Up Angular Service Worker
Step 1: Create or Open an Angular Project
If you're starting from scratch, create a new project:
ng new offline-angular-app
cd offline-angular-app
Step 2: Add PWA Support
Add the Angular service worker package to your project:
ng add @angular/pwa
This command performs several important tasks:
- Adds the
@angular/service-worker
package to your project - Enables service worker support in the Angular CLI
- Creates a default service worker configuration file (
ngsw-config.json
) - Updates your
app.module.ts
to import and register the service worker - Creates icons for your PWA
- Updates your
index.html
with a web app manifest and meta tags
Let's examine what was added to your app.module.ts
:
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
@NgModule({
imports: [
// ... other imports
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first)
registrationStrategy: 'registerWhenStable:30000'
})
],
// ... rest of module configuration
})
export class AppModule { }
Understanding the Service Worker Configuration
The ngsw-config.json
file controls how your application behaves offline. Let's look at its default structure:
{
"$schema": "./node_modules/@angular/service-worker/config/schema.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/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
]
}
This configuration defines two asset groups:
-
App: Core application files that are prefetched and cached when the service worker is installed. These are essential files needed to run your application.
-
Assets: Non-critical files like images and fonts that are cached lazily (when they're first requested) but updated eagerly.
Implementing Basic Offline Support
Let's build a simple application that demonstrates offline capabilities.
Create a Component for Offline Detection
First, let's create a component that shows the current network status:
ng generate component network-status
Update the network-status.component.ts
:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-network-status',
template: `
<div [ngClass]="{'online': isOnline, 'offline': !isOnline}">
Network Status: <strong>{{ isOnline ? 'Online' : 'Offline' }}</strong>
</div>
`,
styles: [`
.online {
padding: 10px;
background-color: #dff0d8;
color: #3c763d;
border-radius: 4px;
margin: 10px 0;
}
.offline {
padding: 10px;
background-color: #f2dede;
color: #a94442;
border-radius: 4px;
margin: 10px 0;
}
`]
})
export class NetworkStatusComponent implements OnInit {
isOnline = navigator.onLine;
constructor() {}
ngOnInit(): void {
this.setupNetworkListeners();
}
setupNetworkListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
}
Handling Service Worker Updates
Add a component to notify users when updates are available:
ng generate component update-notification
Update update-notification.component.ts
:
import { Component, OnInit } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
@Component({
selector: 'app-update-notification',
template: `
<div *ngIf="updateAvailable" class="update-notification">
A new version of this app is available.
<button (click)="updateApp()">Update Now</button>
</div>
`,
styles: [`
.update-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: #3f51b5;
color: white;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
button {
margin-left: 10px;
padding: 5px 10px;
background: white;
color: #3f51b5;
border: none;
border-radius: 4px;
cursor: pointer;
}
`]
})
export class UpdateNotificationComponent implements OnInit {
updateAvailable = false;
constructor(private swUpdate: SwUpdate) {}
ngOnInit(): void {
if (this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => {
this.updateAvailable = true;
});
}
}
updateApp() {
this.swUpdate.activateUpdate().then(() => document.location.reload());
}
}
Add Components to App Component
Update app.component.html
to include these components:
<div class="container">
<h1>Angular Offline Support Demo</h1>
<app-network-status></app-network-status>
<app-update-notification></app-update-notification>
<div class="content">
<p>Try turning off your network connection to see how the application behaves offline!</p>
<p>Content loaded from cache will still be available even when you're offline.</p>
</div>
</div>
Caching API Responses for Offline Use
A crucial aspect of offline support is caching API responses. Let's implement this:
Step 1: Update Service Worker Configuration
Modify ngsw-config.json
to include a data group for API caching:
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
// ... existing asset groups
],
"dataGroups": [
{
"name": "api-cache",
"urls": [
"/api/posts",
"/api/users"
],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 100,
"maxAge": "1h",
"timeout": "10s"
}
}
]
}
This configuration defines:
- URLs to cache: API endpoints that should be cached
- Strategy: "freshness" (network-first) or "performance" (cache-first)
- maxSize: Maximum number of responses to store
- maxAge: How long responses remain valid
- timeout: How long to wait for network before using cache
Step 2: Create a Service for Data Fetching
ng generate service services/data
Implement the service with offline-aware data fetching:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
private localStorageKey = 'offline_data_backup';
private apiUrl = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) {}
getPosts(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl).pipe(
tap(data => {
// Save data to localStorage as backup for complete offline scenarios
localStorage.setItem(this.localStorageKey, JSON.stringify(data));
}),
catchError(() => {
// If request fails, try to load from localStorage
const cachedData = localStorage.getItem(this.localStorageKey);
if (cachedData) {
return of(JSON.parse(cachedData));
}
return of([]);
})
);
}
}
Step 3: Create a Component to Display Posts
ng generate component posts
Update posts.component.ts
:
import { Component, OnInit } from '@angular/core';
import { DataService } from '../services/data.service';
@Component({
selector: 'app-posts',
template: `
<div class="posts-container">
<h2>Posts</h2>
<div *ngIf="loading">Loading posts...</div>
<div *ngIf="error" class="error">
{{ error }}
</div>
<div class="posts-list">
<div *ngFor="let post of posts" class="post-card">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
</div>
</div>
`,
styles: [`
.posts-container {
margin: 20px 0;
}
.post-card {
background: #f9f9f9;
margin-bottom: 15px;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #3f51b5;
}
.error {
color: red;
padding: 10px;
background: #ffeeee;
border-radius: 4px;
}
`]
})
export class PostsComponent implements OnInit {
posts: any[] = [];
loading = true;
error: string | null = null;
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.fetchPosts();
}
fetchPosts() {
this.dataService.getPosts().subscribe(
data => {
this.posts = data.slice(0, 10); // Just showing first 10 posts
this.loading = false;
},
error => {
this.error = 'Failed to load posts. You might be offline.';
this.loading = false;
}
);
}
}
Add the posts component to your app component:
<div class="container">
<!-- Existing content -->
<app-posts></app-posts>
</div>
Testing Offline Support
To test your application's offline capabilities:
-
Build the application for production:
bashng build --prod
-
Serve the built application:
bashnpx http-server -p 8080 -c-1 dist/offline-angular-app
-
Open the application in a browser at http://localhost:8080
-
Use your browser's developer tools to:
- Check that the service worker is registered (Application tab > Service Workers)
- View cached resources (Application tab > Cache Storage)
- Test offline mode (Network tab > Offline)
Advanced Offline Features
Custom Offline Page
Create a component for an offline fallback:
ng generate component offline-fallback
Update offline-fallback.component.ts
:
import { Component } from '@angular/core';
@Component({
selector: 'app-offline-fallback',
template: `
<div class="offline-container">
<h2>You're Offline</h2>
<p>The page you're trying to access can't be loaded while you're offline.</p>
<p>Please check your connection and try again.</p>
</div>
`,
styles: [`
.offline-container {
text-align: center;
padding: 30px;
background: #f8f8f8;
border-radius: 8px;
margin: 20px 0;
}
h2 {
color: #3f51b5;
}
`]
})
export class OfflineFallbackComponent {}
Handle Offline State in Your App
Create an offline service to centralize offline state management:
ng generate service services/offline
Update offline.service.ts
:
import { Injectable } from '@angular/core';
import { BehaviorSubject, fromEvent, Observable, merge } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class OfflineService {
private online$ = new BehaviorSubject<boolean>(navigator.onLine);
constructor() {
// Listen to online/offline events and update the BehaviorSubject
merge(
fromEvent(window, 'online').pipe(map(() => true)),
fromEvent(window, 'offline').pipe(map(() => false))
).subscribe(online => this.online$.next(online));
}
get isOnline(): boolean {
return this.online$.getValue();
}
get onlineStatus$(): Observable<boolean> {
return this.online$.asObservable();
}
// Method to check if data should be fetched from network or cache
shouldUseOfflineData(): boolean {
return !this.isOnline;
}
}
Best Practices for Offline-First Applications
-
Progressive Enhancement: Design your app to work without JavaScript or service workers first, then enhance with offline capabilities.
-
Regular Updates: Implement a strategy for updating cached content regularly to prevent stale data.
-
Clear Offline Indicators: Always show users when they are working offline to set expectations.
-
Sync on Reconnect: Queue user actions performed while offline and sync them when connectivity returns.
-
Versioned APIs: Ensure your API versioning strategy accounts for cached responses.
-
Cache Size Management: Be mindful of cache size limitations and implement strategies to manage large data sets.
-
Background Sync: Use background sync APIs for tasks that can be deferred until connectivity is restored.
Example background sync implementation:
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
@Injectable({
providedIn: 'root'
})
export class SyncService {
private readonly SYNC_QUEUE_KEY = 'offline_sync_queue';
constructor(private swUpdate: SwUpdate) {}
addToSyncQueue(action: any): void {
const queue = this.getSyncQueue();
queue.push({
...action,
timestamp: new Date().getTime()
});
localStorage.setItem(this.SYNC_QUEUE_KEY, JSON.stringify(queue));
}
getSyncQueue(): any[] {
const queueJson = localStorage.getItem(this.SYNC_QUEUE_KEY);
return queueJson ? JSON.parse(queueJson) : [];
}
processQueue(): void {
const queue = this.getSyncQueue();
if (queue.length === 0) return;
// Process each queued item
// This is a simplified example - you would normally process each item
// and remove it from the queue only after successful processing
console.log('Processing sync queue:', queue);
localStorage.setItem(this.SYNC_QUEUE_KEY, JSON.stringify([]));
}
setupSync(): void {
// Process queue when coming online
window.addEventListener('online', () => {
this.processQueue();
});
}
}
Summary
In this tutorial, we've covered:
- Setting up Angular service workers for offline support
- Configuring caching strategies for assets and API responses
- Implementing offline detection and notifications
- Building offline-friendly data services
- Handling application updates
- Best practices for offline-first development
Implementing comprehensive offline support significantly improves the user experience of your Angular applications, making them more resilient and reliable even in challenging network environments.
Further Learning Resources
- Angular Service Worker Documentation
- MDN Web Docs: Service Worker API
- Google Developers: Offline Cookbook
- Workbox: JavaScript Libraries for Service Workers
Exercises
-
Basic: Modify the
ngsw-config.json
file to cache additional asset types specific to your application. -
Intermediate: Implement a "force refresh" button that bypasses the cache and gets fresh data from the network.
-
Advanced: Create an offline queue system that stores user actions (like form submissions) performed while offline and processes them when connectivity is restored.
-
Expert: Build a complete offline-first application with data synchronization between client and server, handling conflict resolution when the same data is modified offline by different users.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)