Angular Common provides utilities for platform detection, viewport scrolling management, DOM access, and environment-specific functionality to enable universal applications that work across different execution contexts.
Functions to detect the current execution environment.
/**
* Check if running in browser environment
* Returns true when code is executing in a web browser
*/
export function isPlatformBrowser(platformId: Object): boolean;
/**
* Check if running in server environment
* Returns true when code is executing on the server (SSR)
*/
export function isPlatformServer(platformId: Object): boolean;
/**
* Check if running in web worker app environment
* Returns true when code is executing in a web worker
*/
export function isPlatformWorkerApp(platformId: Object): boolean;
/**
* Check if running in web worker UI environment
* Returns true when code is executing in a web worker UI context
*/
export function isPlatformWorkerUi(platformId: Object): boolean;Usage Examples:
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
@Injectable()
export class PlatformService {
private isBrowser: boolean;
private isServer: boolean;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
this.isBrowser = isPlatformBrowser(this.platformId);
this.isServer = isPlatformServer(this.platformId);
}
// Browser-only operations
accessLocalStorage(): any {
if (this.isBrowser) {
return localStorage.getItem('user-data');
}
return null;
}
// Server-only operations
logServerInfo(): void {
if (this.isServer) {
console.log('Server-side rendering active');
}
}
// Universal operations with platform-specific logic
getCurrentUrl(): string {
if (this.isBrowser) {
return window.location.href;
} else if (this.isServer) {
// Use server-provided URL or default
return 'https://example.com';
}
return '';
}
// Feature detection based on platform
supportsWebGL(): boolean {
if (!this.isBrowser) return false;
try {
const canvas = document.createElement('canvas');
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
} catch {
return false;
}
}
}
@Component({
template: `
<div *ngIf="showBrowserFeatures">
<button (click)="saveToLocalStorage()">Save Data</button>
<canvas #gameCanvas></canvas>
</div>
<div *ngIf="!showBrowserFeatures">
<p>Browser features not available</p>
</div>
`
})
export class PlatformAwareComponent {
showBrowserFeatures: boolean;
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
private platformService: PlatformService
) {
this.showBrowserFeatures = isPlatformBrowser(this.platformId);
}
saveToLocalStorage() {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem('timestamp', Date.now().toString());
}
}
ngAfterViewInit() {
if (this.showBrowserFeatures && this.platformService.supportsWebGL()) {
// Initialize WebGL canvas
this.initializeWebGL();
}
}
private initializeWebGL() {
// WebGL initialization code
}
}
// Service worker detection
@Injectable()
export class ServiceWorkerService {
private isWorker: boolean;
private isWorkerUi: boolean;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
this.isWorker = isPlatformWorkerApp(this.platformId);
this.isWorkerUi = isPlatformWorkerUi(this.platformId);
}
initializeWorkerFeatures() {
if (this.isWorker) {
// Initialize web worker specific features
this.setupWorkerMessageHandling();
}
if (this.isWorkerUi) {
// Initialize web worker UI specific features
this.setupWorkerUIFeatures();
}
}
private setupWorkerMessageHandling() {
// Worker message handling logic
}
private setupWorkerUIFeatures() {
// Worker UI features
}
}
// Universal service with platform detection
@Injectable()
export class UniversalHttpService {
private baseUrl: string;
constructor(
private http: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object
) {
if (isPlatformServer(this.platformId)) {
// Use absolute URL for server-side requests
this.baseUrl = 'https://api.example.com';
} else {
// Use relative URL for client-side requests
this.baseUrl = '/api';
}
}
getData(): Observable<any> {
return this.http.get(`${this.baseUrl}/data`);
}
// Platform-specific caching
getCachedData(): Observable<any> {
if (isPlatformBrowser(this.platformId)) {
// Use browser cache/local storage
const cached = localStorage.getItem('cached-data');
if (cached) {
return of(JSON.parse(cached));
}
}
// Fallback to HTTP request
return this.getData().pipe(
tap(data => {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem('cached-data', JSON.stringify(data));
}
})
);
}
}Service for managing viewport scrolling behavior.
/**
* Abstract service for managing viewport scrolling
* Provides cross-platform scrolling capabilities
*/
export abstract class ViewportScroller {
/** Set scroll offset for anchor navigation */
abstract setOffset(offset: [number, number] | (() => [number, number])): void;
/** Get current scroll position */
abstract getScrollPosition(): [number, number];
/** Scroll to specific position */
abstract scrollToPosition(position: [number, number]): void;
/** Scroll to element with given anchor */
abstract scrollToAnchor(anchor: string): void;
/** Set browser scroll restoration behavior */
abstract setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void;
}
/**
* Browser implementation of ViewportScroller
* Uses window and document APIs for scrolling
*/
export class BrowserViewportScroller implements ViewportScroller {
setOffset(offset: [number, number] | (() => [number, number])): void;
getScrollPosition(): [number, number];
scrollToPosition(position: [number, number]): void;
scrollToAnchor(anchor: string): void;
setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void;
}
/**
* Null implementation for non-browser environments
* Provides no-op implementations for server-side rendering
*/
export class NullViewportScroller implements ViewportScroller {
setOffset(offset: [number, number] | (() => [number, number])): void;
getScrollPosition(): [number, number];
scrollToPosition(position: [number, number]): void;
scrollToAnchor(anchor: string): void;
setHistoryScrollRestoration(scrollRestoration: 'auto' | 'manual'): void;
}Usage Examples:
@Injectable()
export class ScrollService {
constructor(private viewportScroller: ViewportScroller) {}
// Scroll to top of page
scrollToTop() {
this.viewportScroller.scrollToPosition([0, 0]);
}
// Scroll to specific coordinates
scrollToCoordinates(x: number, y: number) {
this.viewportScroller.scrollToPosition([x, y]);
}
// Scroll to anchor element
scrollToSection(sectionId: string) {
this.viewportScroller.scrollToAnchor(sectionId);
}
// Get current scroll position
getCurrentScrollPosition(): [number, number] {
return this.viewportScroller.getScrollPosition();
}
// Set offset for anchor scrolling (useful for fixed headers)
setScrollOffset(x: number, y: number) {
this.viewportScroller.setOffset([x, y]);
}
// Set dynamic offset based on header height
setDynamicOffset() {
this.viewportScroller.setOffset(() => {
const header = document.querySelector('header');
const headerHeight = header ? header.offsetHeight : 0;
return [0, headerHeight + 20]; // Add some padding
});
}
// Configure scroll restoration
configureScrollRestoration(mode: 'auto' | 'manual') {
this.viewportScroller.setHistoryScrollRestoration(mode);
}
// Smooth scroll to element
smoothScrollToElement(elementId: string) {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
}
}
// Check if element is in viewport
isElementInViewport(elementId: string): boolean {
const element = document.getElementById(elementId);
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}
// Scroll element into view if not visible
scrollToElementIfNeeded(elementId: string) {
if (!this.isElementInViewport(elementId)) {
this.scrollToSection(elementId);
}
}
}
@Component({
template: `
<nav class="fixed-header">
<button (click)="scrollToSection('section1')">Section 1</button>
<button (click)="scrollToSection('section2')">Section 2</button>
<button (click)="scrollToTop()">Top</button>
</nav>
<main>
<section id="section1">
<h2>Section 1</h2>
<p>Content...</p>
</section>
<section id="section2">
<h2>Section 2</h2>
<p>Content...</p>
</section>
</main>
<div class="scroll-info">
Position: {{ scrollPosition[0] }}, {{ scrollPosition[1] }}
</div>
`
})
export class ScrollablePageComponent implements OnInit, OnDestroy {
scrollPosition: [number, number] = [0, 0];
private scrollSubscription?: Subscription;
constructor(private scrollService: ScrollService) {}
ngOnInit() {
// Set offset for fixed header
this.scrollService.setDynamicOffset();
// Configure scroll restoration
this.scrollService.configureScrollRestoration('manual');
// Monitor scroll position
this.scrollSubscription = interval(100).subscribe(() => {
this.scrollPosition = this.scrollService.getCurrentScrollPosition();
});
}
ngOnDestroy() {
this.scrollSubscription?.unsubscribe();
}
scrollToSection(sectionId: string) {
this.scrollService.scrollToSection(sectionId);
}
scrollToTop() {
this.scrollService.scrollToTop();
}
scrollToCoordinates() {
this.scrollService.scrollToCoordinates(0, 500);
}
}
// Router integration with scroll management
@Injectable()
export class RouterScrollService {
constructor(
private router: Router,
private viewportScroller: ViewportScroller
) {
this.setupScrollBehavior();
}
private setupScrollBehavior() {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
// Handle fragment-based navigation
if (event.urlAfterRedirects.includes('#')) {
const fragment = event.urlAfterRedirects.split('#')[1];
setTimeout(() => {
this.viewportScroller.scrollToAnchor(fragment);
}, 100);
} else {
// Scroll to top for regular navigation
this.viewportScroller.scrollToPosition([0, 0]);
}
});
}
}Injection token for accessing the Document object.
/**
* Injection token for the Document object
* Provides safe access to document in universal applications
*/
export const DOCUMENT: InjectionToken<Document>;Usage Examples:
@Injectable()
export class DomService {
constructor(@Inject(DOCUMENT) private document: Document) {}
// Safe document access
createElement(tagName: string): HTMLElement {
return this.document.createElement(tagName);
}
// Query DOM elements
getElementById(id: string): HTMLElement | null {
return this.document.getElementById(id);
}
querySelector(selector: string): Element | null {
return this.document.querySelector(selector);
}
querySelectorAll(selector: string): NodeListOf<Element> {
return this.document.querySelectorAll(selector);
}
// Document properties
getTitle(): string {
return this.document.title;
}
setTitle(title: string): void {
this.document.title = title;
}
getUrl(): string {
return this.document.URL;
}
// Cookie operations
getCookie(name: string): string | null {
const value = `; ${this.document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(';').shift() || null;
}
return null;
}
setCookie(name: string, value: string, days?: number): void {
let expires = '';
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = `; expires=${date.toUTCString()}`;
}
this.document.cookie = `${name}=${value}${expires}; path=/`;
}
// Style manipulation
addStylesheet(href: string): void {
const link = this.document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
this.document.head.appendChild(link);
}
// Meta tag operations
setMetaTag(name: string, content: string): void {
let meta = this.document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement;
if (!meta) {
meta = this.document.createElement('meta') as HTMLMetaElement;
meta.name = name;
this.document.head.appendChild(meta);
}
meta.content = content;
}
getMetaTag(name: string): string | null {
const meta = this.document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement;
return meta ? meta.content : null;
}
}
@Component({})
export class DomManipulationComponent {
constructor(
@Inject(DOCUMENT) private document: Document,
private domService: DomService
) {}
ngOnInit() {
// Set page title
this.domService.setTitle('My Angular App');
// Set meta tags for SEO
this.domService.setMetaTag('description', 'Angular application with DOM utilities');
this.domService.setMetaTag('keywords', 'Angular, TypeScript, DOM');
// Add custom stylesheet
this.domService.addStylesheet('/assets/custom.css');
}
manipulateDOM() {
// Create and append element
const div = this.domService.createElement('div');
div.textContent = 'Dynamically created element';
div.className = 'dynamic-content';
const container = this.domService.getElementById('dynamic-container');
if (container) {
container.appendChild(div);
}
}
manageCookies() {
// Set cookie
this.domService.setCookie('user-preference', 'dark-theme', 30);
// Get cookie
const preference = this.domService.getCookie('user-preference');
console.log('User preference:', preference);
}
queryDOM() {
// Query multiple elements
const buttons = this.domService.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', () => {
console.log(`Button ${index} clicked`);
});
});
}
}
// Universal document service
@Injectable()
export class UniversalDocumentService {
constructor(
@Inject(DOCUMENT) private document: Document,
@Inject(PLATFORM_ID) private platformId: Object
) {}
// Safe document operations that work in both browser and server
safeGetElementById(id: string): HTMLElement | null {
if (isPlatformBrowser(this.platformId)) {
return this.document.getElementById(id);
}
return null;
}
safeAddEventListener(elementId: string, event: string, handler: EventListener): void {
if (isPlatformBrowser(this.platformId)) {
const element = this.document.getElementById(elementId);
if (element) {
element.addEventListener(event, handler);
}
}
}
// Safe cookie operations
safeCookieAccess(): boolean {
return isPlatformBrowser(this.platformId) && !!this.document.cookie;
}
getClientInfo(): any {
if (isPlatformBrowser(this.platformId)) {
return {
userAgent: navigator.userAgent,
url: this.document.URL,
title: this.document.title,
referrer: this.document.referrer
};
}
return {
userAgent: 'Server',
url: 'Unknown',
title: 'Server Rendered',
referrer: ''
};
}
}Functions for parsing and handling cookies.
/**
* Parse cookie value from cookie string
* Internal utility function (private export)
*/
export function ɵparseCookieValue(cookieStr: string, name: string): string | null;Factory for creating XMLHttpRequest instances.
/**
* Abstract factory for creating XMLHttpRequest objects
* Allows customization of XHR creation for testing or special needs
*/
export abstract class XhrFactory {
abstract build(): XMLHttpRequest;
}Usage Examples:
// Custom XHR factory for testing
@Injectable()
export class MockXhrFactory extends XhrFactory {
build(): XMLHttpRequest {
// Return mock XMLHttpRequest for testing
return new MockXMLHttpRequest() as any;
}
}
// Custom XHR factory with additional configuration
@Injectable()
export class ConfiguredXhrFactory extends XhrFactory {
build(): XMLHttpRequest {
const xhr = new XMLHttpRequest();
// Add custom configuration
xhr.timeout = 30000; // 30 second timeout
// Add common headers
xhr.addEventListener('loadstart', () => {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
});
return xhr;
}
}
// Cookie parsing utility
@Injectable()
export class CookieService {
constructor(@Inject(DOCUMENT) private document: Document) {}
// Parse cookie using Angular's internal utility
private parseCookieValue(name: string): string | null {
// Note: ɵparseCookieValue is internal, use at your own risk
return (ɵparseCookieValue as any)(this.document.cookie, name);
}
// Safe cookie parsing
getCookie(name: string): string | null {
try {
const value = `; ${this.document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop()?.split(';').shift() || '');
}
} catch (error) {
console.error('Error parsing cookie:', error);
}
return null;
}
setCookie(name: string, value: string, options: CookieOptions = {}): void {
let cookieString = `${name}=${encodeURIComponent(value)}`;
if (options.expires) {
cookieString += `; expires=${options.expires.toUTCString()}`;
}
if (options.path) {
cookieString += `; path=${options.path}`;
}
if (options.domain) {
cookieString += `; domain=${options.domain}`;
}
if (options.secure) {
cookieString += '; secure';
}
if (options.sameSite) {
cookieString += `; samesite=${options.sameSite}`;
}
this.document.cookie = cookieString;
}
deleteCookie(name: string, path?: string): void {
this.setCookie(name, '', {
expires: new Date('1970-01-01'),
path: path
});
}
getAllCookies(): { [key: string]: string } {
const cookies: { [key: string]: string } = {};
if (this.document.cookie) {
this.document.cookie.split(';').forEach(cookie => {
const [name, value] = cookie.trim().split('=');
if (name && value) {
cookies[name] = decodeURIComponent(value);
}
});
}
return cookies;
}
}
interface CookieOptions {
expires?: Date;
path?: string;
domain?: string;
secure?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
}
// Platform-aware utility service
@Injectable()
export class PlatformUtilityService {
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(DOCUMENT) private document: Document
) {}
// Environment checks
isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
isServer(): boolean {
return isPlatformServer(this.platformId);
}
isWebWorker(): boolean {
return isPlatformWorkerApp(this.platformId) || isPlatformWorkerUi(this.platformId);
}
// Feature detection
hasLocalStorage(): boolean {
if (!this.isBrowser()) return false;
try {
const test = 'test';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
}
hasSessionStorage(): boolean {
if (!this.isBrowser()) return false;
try {
const test = 'test';
sessionStorage.setItem(test, test);
sessionStorage.removeItem(test);
return true;
} catch {
return false;
}
}
hasCookieSupport(): boolean {
if (!this.isBrowser()) return false;
try {
this.document.cookie = 'test=test';
const hasCookie = this.document.cookie.indexOf('test=test') !== -1;
this.document.cookie = 'test=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
return hasCookie;
} catch {
return false;
}
}
// Utility methods
getEnvironmentInfo(): any {
return {
platform: {
browser: this.isBrowser(),
server: this.isServer(),
webWorker: this.isWebWorker()
},
features: {
localStorage: this.hasLocalStorage(),
sessionStorage: this.hasSessionStorage(),
cookies: this.hasCookieSupport()
},
userAgent: this.isBrowser() ? navigator.userAgent : 'Server'
};
}
}// Platform ID constants (private exports)
export const ɵPLATFORM_BROWSER_ID: string;
export const ɵPLATFORM_SERVER_ID: string;
export const ɵPLATFORM_WORKER_APP_ID: string;
export const ɵPLATFORM_WORKER_UI_ID: string;
// Injection tokens
export const DOCUMENT: InjectionToken<Document>;
// Version information
export const VERSION: Version;