@angular-helpers/browser-web-apis 21.1.0 → 21.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,14 +23,61 @@ Angular services package for a structured and secure access layer over browser W
23
23
  - `MediaDevicesService` - Media device listing and management
24
24
  - `GeolocationService` - Geolocation API access
25
25
  - `NotificationService` - Browser notifications API
26
+ - `MediaRecorderService` - Record audio/video from MediaStream
26
27
 
27
- ### Web APIs
28
+ ### Observer APIs
29
+
30
+ - `IntersectionObserverService` - Detect when elements enter/exit viewport
31
+ - `ResizeObserverService` - Watch for element size changes
32
+ - `MutationObserverService` - Watch for DOM mutations
33
+ - `PerformanceObserverService` - Monitor performance entries (LCP, CLS, etc.)
34
+
35
+ ### System APIs
36
+
37
+ - `BatteryService` - Monitor battery status and charging state
38
+ - `PageVisibilityService` - Track document visibility state changes
39
+ - `ScreenWakeLockService` - Prevent screen from dimming or locking
40
+ - `ScreenOrientationService` - Read and lock screen orientation
41
+ - `FullscreenService` - Toggle fullscreen mode for elements
42
+ - `VibrationService` - Trigger haptic feedback patterns
43
+ - `SpeechSynthesisService` - Text-to-speech with voice selection
44
+ - `IdleDetectorService` - Detect user idle state and screen lock
45
+ - `GamepadService` - Game controller input polling
46
+ - `WebAudioService` - Audio context, oscillators, and analysers
47
+
48
+ ### Network APIs
28
49
 
29
- - `WebWorkerService` - Web Worker management
30
50
  - `WebSocketService` - WebSocket connection handling
51
+ - `ServerSentEventsService` - Server-Sent Events client
52
+ - `BroadcastChannelService` - Inter-tab communication
53
+ - `NetworkInformationService` - Connection info and online status
54
+
55
+ ### Storage & I/O APIs
56
+
31
57
  - `WebStorageService` - LocalStorage and SessionStorage helpers
32
58
  - `WebShareService` - Native Web Share API support
33
59
  - `ClipboardService` - System clipboard access
60
+ - `FileSystemAccessService` - Open/save files via native picker
61
+
62
+ ### Web APIs
63
+
64
+ - `WebWorkerService` - Web Worker management
65
+
66
+ ### Device APIs
67
+
68
+ - `WebBluetoothService` - Bluetooth Low Energy device communication
69
+ - `WebUsbService` - USB device I/O from the browser
70
+ - `WebNfcService` - NFC tag reading and writing
71
+
72
+ ### Detection APIs
73
+
74
+ - `EyeDropperService` - Screen color picker
75
+ - `BarcodeDetectorService` - QR code and barcode scanning
76
+
77
+ ### Commerce & Identity APIs
78
+
79
+ - `PaymentRequestService` - Native payment flows
80
+ - `CredentialManagementService` - Passwords, passkeys (WebAuthn)
34
81
 
35
82
  ### Security & Capabilities
36
83
 
@@ -132,6 +179,536 @@ export class LocationComponent {
132
179
  }
133
180
  ```
134
181
 
182
+ ### IntersectionObserverService
183
+
184
+ ```typescript
185
+ import { IntersectionObserverService } from '@angular-helpers/browser-web-apis';
186
+
187
+ export class LazyImageComponent {
188
+ private intersectionService = inject(IntersectionObserverService);
189
+ private elementRef = inject(ElementRef);
190
+
191
+ isVisible = signal(false);
192
+
193
+ ngAfterViewInit() {
194
+ this.intersectionService
195
+ .observeVisibility(this.elementRef.nativeElement, { threshold: 0.5 })
196
+ .subscribe((visible) => this.isVisible.set(visible));
197
+ }
198
+ }
199
+ ```
200
+
201
+ ### ResizeObserverService
202
+
203
+ ```typescript
204
+ import { ResizeObserverService } from '@angular-helpers/browser-web-apis';
205
+
206
+ export class ResponsiveComponent {
207
+ private resizeService = inject(ResizeObserverService);
208
+ private elementRef = inject(ElementRef);
209
+
210
+ elementSize = signal<ElementSize | null>(null);
211
+
212
+ ngAfterViewInit() {
213
+ this.resizeService
214
+ .observeSize(this.elementRef.nativeElement)
215
+ .subscribe((size) => this.elementSize.set(size));
216
+ }
217
+ }
218
+ ```
219
+
220
+ ### PageVisibilityService
221
+
222
+ ```typescript
223
+ import { PageVisibilityService } from '@angular-helpers/browser-web-apis';
224
+
225
+ export class AnalyticsComponent {
226
+ private visibilityService = inject(PageVisibilityService);
227
+
228
+ ngOnInit() {
229
+ this.visibilityService.watch().subscribe((state) => {
230
+ console.log('Page is now:', state);
231
+ });
232
+ }
233
+ }
234
+ ```
235
+
236
+ ### FullscreenService
237
+
238
+ ```typescript
239
+ import { FullscreenService } from '@angular-helpers/browser-web-apis';
240
+
241
+ export class VideoPlayerComponent {
242
+ private fullscreenService = inject(FullscreenService);
243
+
244
+ async toggleFullscreen() {
245
+ await this.fullscreenService.toggle();
246
+ }
247
+ }
248
+ ```
249
+
250
+ ### ScreenWakeLockService
251
+
252
+ ```typescript
253
+ import { ScreenWakeLockService } from '@angular-helpers/browser-web-apis';
254
+
255
+ export class PresentationComponent {
256
+ private wakeLockService = inject(ScreenWakeLockService);
257
+
258
+ async keepScreenOn() {
259
+ await this.wakeLockService.request();
260
+ }
261
+
262
+ async releaseScreen() {
263
+ await this.wakeLockService.release();
264
+ }
265
+ }
266
+ ```
267
+
268
+ ### BroadcastChannelService
269
+
270
+ ```typescript
271
+ import { BroadcastChannelService } from '@angular-helpers/browser-web-apis';
272
+
273
+ export class SyncComponent {
274
+ private broadcastService = inject(BroadcastChannelService);
275
+
276
+ ngOnInit() {
277
+ // Listen for messages from other tabs
278
+ this.broadcastService.open<string>('app-sync').subscribe((msg) => {
279
+ console.log('Received:', msg);
280
+ });
281
+ }
282
+
283
+ sendMessage(data: string) {
284
+ this.broadcastService.post('app-sync', data);
285
+ }
286
+ }
287
+ ```
288
+
289
+ ### ServerSentEventsService
290
+
291
+ ```typescript
292
+ import { ServerSentEventsService } from '@angular-helpers/browser-web-apis';
293
+
294
+ export class LiveFeedComponent {
295
+ private sseService = inject(ServerSentEventsService);
296
+
297
+ connectToEvents() {
298
+ this.sseService.connect('https://api.example.com/events').subscribe({
299
+ next: (message) => console.log('Event:', message),
300
+ error: (err) => console.error('SSE error:', err),
301
+ });
302
+ }
303
+ }
304
+ ```
305
+
306
+ ### VibrationService
307
+
308
+ ```typescript
309
+ import { VibrationService } from '@angular-helpers/browser-web-apis';
310
+
311
+ export class FeedbackComponent {
312
+ private vibrationService = inject(VibrationService);
313
+
314
+ onSuccess() {
315
+ this.vibrationService.success();
316
+ }
317
+
318
+ onError() {
319
+ this.vibrationService.error();
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### SpeechSynthesisService
325
+
326
+ ```typescript
327
+ import { SpeechSynthesisService } from '@angular-helpers/browser-web-apis';
328
+
329
+ export class VoiceComponent {
330
+ private speechService = inject(SpeechSynthesisService);
331
+
332
+ speakText(text: string) {
333
+ this.speechService.speak(text).subscribe((state) => {
334
+ console.log('Speech state:', state);
335
+ });
336
+ }
337
+ }
338
+ ```
339
+
340
+ ### BatteryService
341
+
342
+ ```typescript
343
+ import { BatteryService } from '@angular-helpers/browser-web-apis';
344
+
345
+ export class PowerComponent {
346
+ private batteryService = inject(BatteryService);
347
+
348
+ async ngOnInit() {
349
+ try {
350
+ // Initialize and get initial battery info
351
+ const batteryInfo = await this.batteryService.initialize();
352
+ console.log('Battery level:', batteryInfo.level);
353
+ console.log('Is charging:', batteryInfo.charging);
354
+
355
+ // Watch for battery changes
356
+ this.batteryService.watchBatteryInfo().subscribe((info) => {
357
+ console.log('Battery updated:', info);
358
+ });
359
+ } catch (error) {
360
+ console.error('Battery API not supported:', error);
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ ### ClipboardService
367
+
368
+ ```typescript
369
+ import { ClipboardService } from '@angular-helpers/browser-web-apis';
370
+
371
+ export class CopyComponent {
372
+ private clipboardService = inject(ClipboardService);
373
+
374
+ async copyToClipboard(text: string) {
375
+ try {
376
+ await this.clipboardService.writeText(text);
377
+ console.log('Copied successfully');
378
+ } catch (error) {
379
+ console.error('Failed to copy:', error);
380
+ }
381
+ }
382
+
383
+ async pasteFromClipboard(): Promise<string> {
384
+ try {
385
+ const text = await this.clipboardService.readText();
386
+ return text;
387
+ } catch (error) {
388
+ console.error('Failed to read clipboard:', error);
389
+ return '';
390
+ }
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### FileSystemAccessService
396
+
397
+ ```typescript
398
+ import { FileSystemAccessService } from '@angular-helpers/browser-web-apis';
399
+
400
+ export class FileManagerComponent {
401
+ private fileService = inject(FileSystemAccessService);
402
+
403
+ async openFiles() {
404
+ try {
405
+ const files = await this.fileService.openFile({
406
+ multiple: true,
407
+ types: [
408
+ {
409
+ description: 'Text files',
410
+ accept: { 'text/plain': ['.txt'] },
411
+ },
412
+ ],
413
+ });
414
+ console.log('Selected files:', files);
415
+ } catch (error) {
416
+ console.error('Failed to open files:', error);
417
+ }
418
+ }
419
+
420
+ async saveContent(content: string) {
421
+ try {
422
+ await this.fileService.saveFile(content, {
423
+ suggestedName: 'document.txt',
424
+ });
425
+ } catch (error) {
426
+ console.error('Failed to save file:', error);
427
+ }
428
+ }
429
+ }
430
+ ```
431
+
432
+ ### MediaRecorderService
433
+
434
+ ```typescript
435
+ import { MediaRecorderService } from '@angular-helpers/browser-web-apis';
436
+
437
+ export class RecorderComponent {
438
+ private recorderService = inject(MediaRecorderService);
439
+ private stream: MediaStream | null = null;
440
+
441
+ async startRecording() {
442
+ try {
443
+ this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
444
+ await this.recorderService.start(this.stream, { mimeType: 'video/webm' });
445
+ } catch (error) {
446
+ console.error('Failed to start recording:', error);
447
+ }
448
+ }
449
+
450
+ stopRecording() {
451
+ const result = this.recorderService.stop();
452
+ if (result) {
453
+ console.log('Recording saved, blob URL:', result.url);
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ ### WebSocketService
460
+
461
+ ```typescript
462
+ import { WebSocketService } from '@angular-helpers/browser-web-apis';
463
+
464
+ export class LiveComponent {
465
+ private wsService = inject(WebSocketService);
466
+
467
+ connect() {
468
+ this.wsService
469
+ .connect({
470
+ url: 'wss://example.com/socket',
471
+ reconnectInterval: 3000,
472
+ maxReconnectAttempts: 5,
473
+ })
474
+ .subscribe((status) => {
475
+ console.log('Connection status:', status);
476
+ });
477
+
478
+ this.wsService.getMessages().subscribe((message) => {
479
+ console.log('Received:', message);
480
+ });
481
+ }
482
+
483
+ sendMessage(data: unknown) {
484
+ this.wsService.send({ type: 'message', data });
485
+ }
486
+ }
487
+ ```
488
+
489
+ ### WebStorageService
490
+
491
+ ```typescript
492
+ import { WebStorageService } from '@angular-helpers/browser-web-apis';
493
+
494
+ export class SettingsComponent {
495
+ private storageService = inject(WebStorageService);
496
+
497
+ saveSetting(key: string, value: unknown) {
498
+ this.storageService.setLocalStorage(key, value);
499
+ }
500
+
501
+ getSetting<T>(key: string): T | null {
502
+ return this.storageService.getLocalStorage<T>(key);
503
+ }
504
+
505
+ watchSetting<T>(key: string) {
506
+ return this.storageService.watchLocalStorage<T>(key).subscribe((value) => {
507
+ console.log('Setting changed:', value);
508
+ });
509
+ }
510
+ }
511
+ ```
512
+
513
+ ### WebWorkerService
514
+
515
+ ```typescript
516
+ import { WebWorkerService } from '@angular-helpers/browser-web-apis';
517
+
518
+ export class WorkerComponent {
519
+ private workerService = inject(WebWorkerService);
520
+
521
+ async createWorker() {
522
+ this.workerService
523
+ .createWorker('calc-worker', '/assets/workers/calc.worker.js')
524
+ .subscribe((status) => {
525
+ console.log('Worker status:', status);
526
+ });
527
+
528
+ this.workerService.getMessages('calc-worker').subscribe((message) => {
529
+ console.log('Worker response:', message);
530
+ });
531
+ }
532
+
533
+ sendTask(data: unknown) {
534
+ this.workerService.postMessage('calc-worker', {
535
+ id: 'task-1',
536
+ type: 'CALCULATE',
537
+ data,
538
+ });
539
+ }
540
+ }
541
+ ```
542
+
543
+ ## Signal Fn Primitives
544
+
545
+ Zero-boilerplate reactive alternatives to the RxJS-based services. Each `inject*` function returns a ref object with read-only signals and handles cleanup automatically via `DestroyRef`.
546
+
547
+ ### injectPageVisibility
548
+
549
+ ```typescript
550
+ import { injectPageVisibility } from '@angular-helpers/browser-web-apis';
551
+
552
+ @Component({...})
553
+ export class MyComponent {
554
+ readonly visibility = injectPageVisibility();
555
+
556
+ // visibility.state() → 'visible' | 'hidden'
557
+ // visibility.isVisible() → boolean
558
+ // visibility.isHidden() → boolean
559
+ }
560
+ ```
561
+
562
+ ### injectResizeObserver
563
+
564
+ Accepts `Element`, `ElementRef`, or a **`Signal<ElementRef | undefined>`** (e.g. from `viewChild`). When a signal is passed, the observer automatically starts when the element becomes available.
565
+
566
+ ```typescript
567
+ import { injectResizeObserver } from '@angular-helpers/browser-web-apis';
568
+
569
+ @Component({...})
570
+ export class MyComponent {
571
+ readonly boxRef = viewChild<ElementRef>('box');
572
+ readonly resize = injectResizeObserver(this.boxRef);
573
+
574
+ // resize.width() → number
575
+ // resize.height() → number
576
+ // resize.inlineSize() → number (logical)
577
+ // resize.blockSize() → number (logical)
578
+ // resize.size() → ElementSize | null
579
+ }
580
+ ```
581
+
582
+ ### injectIntersectionObserver
583
+
584
+ Same `ElementInput` flexibility — works with `viewChild` signals out of the box.
585
+
586
+ ```typescript
587
+ import { injectIntersectionObserver } from '@angular-helpers/browser-web-apis';
588
+
589
+ @Component({...})
590
+ export class MyComponent {
591
+ readonly targetRef = viewChild<ElementRef>('target');
592
+ readonly inView = injectIntersectionObserver(this.targetRef, { threshold: 0.25 });
593
+
594
+ // inView.isIntersecting() → boolean
595
+ // inView.isVisible() → boolean
596
+ }
597
+ ```
598
+
599
+ ### injectNetworkInformation
600
+
601
+ ```typescript
602
+ import { injectNetworkInformation } from '@angular-helpers/browser-web-apis';
603
+
604
+ @Component({...})
605
+ export class MyComponent {
606
+ readonly net = injectNetworkInformation();
607
+
608
+ // net.online() → boolean
609
+ // net.effectiveType() → '4g' | '3g' | '2g' | 'slow-2g' | undefined
610
+ // net.downlink() → number | undefined (Mbps)
611
+ // net.rtt() → number | undefined (ms)
612
+ // net.type() → ConnectionType | undefined
613
+ // net.saveData() → boolean | undefined
614
+ }
615
+ ```
616
+
617
+ ### injectScreenOrientation
618
+
619
+ ```typescript
620
+ import { injectScreenOrientation } from '@angular-helpers/browser-web-apis';
621
+
622
+ @Component({...})
623
+ export class MyComponent {
624
+ readonly orientation = injectScreenOrientation();
625
+
626
+ // orientation.type() → OrientationType
627
+ // orientation.angle() → number
628
+ // orientation.isPortrait() → boolean
629
+ // orientation.isLandscape() → boolean
630
+ // orientation.lock('landscape') → Promise<void>
631
+ // orientation.unlock()
632
+ }
633
+ ```
634
+
635
+ ### injectMutationObserver
636
+
637
+ Accepts the same `ElementInput` type — works with `viewChild` signals.
638
+
639
+ ```typescript
640
+ import { injectMutationObserver } from '@angular-helpers/browser-web-apis';
641
+
642
+ @Component({...})
643
+ export class MyComponent {
644
+ readonly targetRef = viewChild<ElementRef>('target');
645
+ readonly mo = injectMutationObserver(this.targetRef, { childList: true });
646
+
647
+ // mo.mutations() → MutationRecord[]
648
+ // mo.mutationCount() → number
649
+ }
650
+ ```
651
+
652
+ ### injectPerformanceObserver
653
+
654
+ ```typescript
655
+ import { injectPerformanceObserver } from '@angular-helpers/browser-web-apis';
656
+
657
+ @Component({...})
658
+ export class MyComponent {
659
+ readonly perf = injectPerformanceObserver({ type: 'navigation', buffered: true });
660
+
661
+ // perf.entries() → PerformanceEntryList
662
+ // perf.entryCount() → number
663
+ // perf.latestEntry() → PerformanceEntry | undefined
664
+ }
665
+ ```
666
+
667
+ ### injectIdleDetector
668
+
669
+ ```typescript
670
+ import { injectIdleDetector } from '@angular-helpers/browser-web-apis';
671
+
672
+ @Component({...})
673
+ export class MyComponent {
674
+ readonly idle = injectIdleDetector({ threshold: 120_000 });
675
+
676
+ // idle.userState() → 'active' | 'idle'
677
+ // idle.screenState() → 'locked' | 'unlocked'
678
+ // idle.isUserIdle() → boolean
679
+ // idle.isScreenLocked() → boolean
680
+ }
681
+ ```
682
+
683
+ ### injectGamepad
684
+
685
+ ```typescript
686
+ import { injectGamepad } from '@angular-helpers/browser-web-apis';
687
+
688
+ @Component({...})
689
+ export class MyComponent {
690
+ readonly gp = injectGamepad(0);
691
+
692
+ // gp.connected() → boolean
693
+ // gp.buttons() → ReadonlyArray<{ pressed: boolean; value: number }>
694
+ // gp.axes() → readonly number[]
695
+ // gp.state() → GamepadState | null
696
+ }
697
+ ```
698
+
699
+ ### ElementInput type
700
+
701
+ Both `injectResizeObserver` and `injectIntersectionObserver` accept the `ElementInput` type:
702
+
703
+ ```typescript
704
+ type ElementInput =
705
+ | Element
706
+ | ElementRef<Element>
707
+ | Signal<Element | ElementRef<Element> | undefined>;
708
+ ```
709
+
710
+ This means you can pass a `viewChild` signal directly — the function will internally use an `effect` to start observing once the element is rendered, with automatic cleanup.
711
+
135
712
  ## Browser Support
136
713
 
137
714
  The services automatically validate browser support and unsupported-path handling: