-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ported injectNetwork() to Angular (#186)
* feat: ported injectNetwork() to Angular * Update libs/ngxtension/inject-network/src/inject-network.ts Co-authored-by: Chau Tran <[email protected]> * Update libs/ngxtension/inject-network/src/inject-network.ts Co-authored-by: Chau Tran <[email protected]> * Update libs/ngxtension/inject-network/src/inject-network.ts Co-authored-by: Chau Tran <[email protected]> * Update libs/ngxtension/inject-network/src/inject-network.ts Co-authored-by: Chau Tran <[email protected]> * Update libs/ngxtension/inject-network/src/inject-network.ts Co-authored-by: Chau Tran <[email protected]> * Lint fix * adjust implementation --------- Co-authored-by: Chau Tran <[email protected]>
- Loading branch information
1 parent
437aa2c
commit 684a33a
Showing
8 changed files
with
348 additions
and
0 deletions.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
docs/src/content/docs/utilities/Injectors/inject-network.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
--- | ||
title: injectNetwork | ||
description: ngxtension/inject-network | ||
badge: stable | ||
contributor: fiorelozere | ||
--- | ||
|
||
This injector is useful for tracking the current network state of the user. It provides information about the system's connection type, such as 'wifi' or 'cellular'. This utility, along with a singular property added to the Navigator interface (Navigator.connection), allows for the identification of the general type of network connection a system is using. This functionality is particularly useful for choosing between high definition or low definition content depending on the user's network connection. | ||
|
||
```ts | ||
import { injectNetwork } from 'ngxtension/inject-network'; | ||
``` | ||
|
||
## Usage | ||
|
||
`injectNetwork` accepts an optional parameter `options` which can include a custom `window` and an `Injector` instance that are both optional. The `window` parameter is particularly useful for testing scenarios or when needing to track the network state of an iframe. The `injector` allows for dependency injection, providing more flexibility and facilitating testable code by decoupling from the global state or context. | ||
|
||
```ts | ||
const networkState = injectNetwork(); | ||
|
||
effect(() => { | ||
console.log(this.network.type()); | ||
console.log(this.network.downlink()); | ||
console.log(this.network.downlinkMax()); | ||
console.log(this.network.effectiveType()); | ||
console.log(this.network.rtt()); | ||
console.log(this.network.saveData()); | ||
console.log(this.network.online()); | ||
console.log(this.network.offlineAt()); | ||
console.log(this.network.onlineAt()); | ||
console.log(this.network.supported()); | ||
}); | ||
``` | ||
|
||
## API | ||
|
||
```ts | ||
function injectNetwork(options?: InjectNetworkOptions): Readonly<NetworkState>; | ||
``` | ||
|
||
### Parameters | ||
|
||
- `options` (optional): An object that can have the following properties: | ||
- `window`: A custom `Window` instance, defaulting to the global `window` object. | ||
- `injector`: An `Injector` instance for Angular's dependency injection. | ||
|
||
### Returns | ||
|
||
A readonly object with the following properties: | ||
|
||
- `supported`: A signal that emits `true` if the browser supports the Network Information API, otherwise `false`. | ||
- `online`: A signal that emits `true` if the user is online, otherwise `false`. | ||
- `offlineAt`: A signal that emits the time since the user was last connected. | ||
- `onlineAt`: A signal that emits the time since the user was last disconnected. | ||
- `downlink`: A signal that emits the download speed in Mbps. | ||
- `downlinkMax`: A signal that emits the max reachable download speed in Mbps. | ||
- `effectiveType`: A signal that emits the detected effective speed type. | ||
- `rtt`: A signal that emits the estimated effective round-trip time of the current connection. | ||
- `saveData`: A signal that emits `true` if the user activated data saver mode, otherwise `false`. | ||
- `type`: A signal that emits the detected connection/network type. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# ngxtension/inject-network | ||
|
||
Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/inject-network`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"lib": { | ||
"entryFile": "src/index.ts" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "ngxtension/inject-network", | ||
"$schema": "../../../node_modules/nx/schemas/project-schema.json", | ||
"projectType": "library", | ||
"sourceRoot": "libs/ngxtension/inject-network/src", | ||
"targets": { | ||
"test": { | ||
"executor": "@nx/jest:jest", | ||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"], | ||
"options": { | ||
"jestConfig": "libs/ngxtension/jest.config.ts", | ||
"testPathPattern": ["inject-network"], | ||
"passWithNoTests": true | ||
}, | ||
"configurations": { | ||
"ci": { | ||
"ci": true, | ||
"codeCoverage": true | ||
} | ||
} | ||
}, | ||
"lint": { | ||
"executor": "@nx/eslint:lint", | ||
"outputs": ["{options.outputFile}"], | ||
"options": { | ||
"lintFilePatterns": [ | ||
"libs/ngxtension/inject-network/**/*.ts", | ||
"libs/ngxtension/inject-network/**/*.html" | ||
] | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './inject-network'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { Component } from '@angular/core'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { injectNetwork } from './inject-network'; | ||
|
||
describe(injectNetwork.name, () => { | ||
@Component({ | ||
standalone: true, | ||
template: ` | ||
{{ networkState.saveData() }} | ||
{{ networkState.type() }} | ||
{{ networkState.downlink() }} | ||
{{ networkState.downlinkMax() }} | ||
{{ networkState.effectiveType() }} | ||
{{ networkState.rtt() }} | ||
{{ networkState.online() }} | ||
{{ networkState.onlineAt() }} | ||
{{ networkState.offlineAt() }} | ||
`, | ||
}) | ||
class Test { | ||
networkState = injectNetwork(); | ||
} | ||
|
||
function setup() { | ||
const fixture = TestBed.createComponent(Test); | ||
fixture.detectChanges(); | ||
return fixture.componentInstance; | ||
} | ||
|
||
function triggerOnlineEvent() { | ||
// Change the visibility state | ||
Object.defineProperty(navigator, 'onLine', { | ||
writable: true, | ||
configurable: true, | ||
value: true, | ||
}); | ||
|
||
// Dispatch the event | ||
const event = new Event('online'); | ||
window.dispatchEvent(event); | ||
} | ||
|
||
function triggerOfflineEvent() { | ||
// Mock navigator.onLine to return false | ||
Object.defineProperty(navigator, 'onLine', { | ||
value: false, | ||
writable: true, | ||
}); | ||
|
||
// Dispatch the offline event | ||
const offlineEvent = new Event('offline'); | ||
window.dispatchEvent(offlineEvent); | ||
} | ||
|
||
it('should handle online state', () => { | ||
const cmp = setup(); | ||
triggerOnlineEvent(); | ||
expect(cmp.networkState.online()).toEqual(true); | ||
}); | ||
|
||
it('should handle offline state', () => { | ||
const cmp = setup(); | ||
triggerOfflineEvent(); | ||
expect(cmp.networkState.online()).toEqual(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { DOCUMENT } from '@angular/common'; | ||
import { inject, signal, type Injector, type Signal } from '@angular/core'; | ||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; | ||
import { assertInjector } from 'ngxtension/assert-injector'; | ||
import { fromEvent, map, merge, startWith } from 'rxjs'; | ||
|
||
// Ported from https://vueuse.org/core/useNetwork/ | ||
|
||
export type NetworkType = | ||
| 'bluetooth' | ||
| 'cellular' | ||
| 'ethernet' | ||
| 'none' | ||
| 'wifi' | ||
| 'wimax' | ||
| 'other' | ||
| 'unknown'; | ||
|
||
export type NetworkEffectiveType = 'slow-2g' | '2g' | '3g' | '4g' | '5g'; | ||
|
||
export interface NetworkState { | ||
supported: Signal<boolean>; | ||
online: Signal<boolean>; | ||
/** | ||
* The time since the user was last connected. | ||
*/ | ||
offlineAt: Signal<number | undefined>; | ||
/** | ||
* At this time, if the user is offline and reconnects | ||
*/ | ||
onlineAt: Signal<number | undefined>; | ||
/** | ||
* The download speed in Mbps. | ||
*/ | ||
downlink: Signal<number | undefined>; | ||
/** | ||
* The max reachable download speed in Mbps. | ||
*/ | ||
downlinkMax: Signal<number | undefined>; | ||
/** | ||
* The detected effective speed type. | ||
*/ | ||
effectiveType: Signal<NetworkEffectiveType | undefined>; | ||
/** | ||
* The estimated effective round-trip time of the current connection. | ||
*/ | ||
rtt: Signal<number | undefined>; | ||
/** | ||
* If the user activated data saver mode. | ||
*/ | ||
saveData: Signal<boolean | undefined>; | ||
/** | ||
* The detected connection/network type. | ||
*/ | ||
type: Signal<NetworkType>; | ||
} | ||
|
||
export interface InjectNetworkOptions { | ||
injector?: Injector; | ||
window?: Window; | ||
} | ||
|
||
/** | ||
* This injector is useful for tracking the current network state of the user. It provides information about the system's connection type, such as 'wifi' or 'cellular'. This utility, along with a singular property added to the Navigator interface (Navigator.connection), allows for the identification of the general type of network connection a system is using. This functionality is particularly useful for choosing between high definition or low definition content depending on the user's network connection. | ||
* | ||
* @example | ||
* ```ts | ||
* const network = injectNetwork(); | ||
* effect(() => { | ||
* console.log(this.network.type()); | ||
* console.log(this.network.downlink()); | ||
* console.log(this.network.downlinkMax()); | ||
* console.log(this.network.effectiveType()); | ||
* console.log(this.network.rtt()); | ||
* console.log(this.network.saveData()); | ||
* console.log(this.network.online()); | ||
* console.log(this.network.offlineAt()); | ||
* console.log(this.network.onlineAt()); | ||
* console.log(this.network.supported()); | ||
* }); | ||
* ``` | ||
* | ||
* @param options An optional object with the following properties: | ||
* - `window`: (Optional) Specifies a custom `Window` instance. This is useful when working with iframes or in testing environments where the global `window` might not be appropriate. | ||
* - `injector`: (Optional) Specifies a custom `Injector` instance for dependency injection. This allows for more flexible and testable code by decoupling from a global state or context. | ||
* | ||
* @returns A readonly object with the following properties: | ||
* - `supported`: A signal that emits `true` if the browser supports the Network Information API, otherwise `false`. | ||
* - `online`: A signal that emits `true` if the user is online, otherwise `false`. | ||
* - `offlineAt`: A signal that emits the time since the user was last connected. | ||
* - `onlineAt`: A signal that emits the time since the user was last disconnected. | ||
* - `downlink`: A signal that emits the download speed in Mbps. | ||
* - `downlinkMax`: A signal that emits the max reachable download speed in Mbps. | ||
* - `effectiveType`: A signal that emits the detected effective speed type. | ||
* - `rtt`: A signal that emits the estimated effective round-trip time of the current connection. | ||
* - `saveData`: A signal that emits `true` if the user activated data saver mode, otherwise `false`. | ||
* - `type`: A signal that emits the detected connection/network type. | ||
*/ | ||
export function injectNetwork({ | ||
injector, | ||
window: customWindow, | ||
}: InjectNetworkOptions = {}): Readonly<NetworkState> { | ||
return assertInjector(injectNetwork, injector, () => { | ||
const window: Window = customWindow ?? inject(DOCUMENT).defaultView!; | ||
const navigator = window?.navigator; | ||
|
||
const supported = signal( | ||
window?.navigator && 'connection' in window.navigator | ||
); | ||
|
||
const online = signal(true); | ||
const saveData = signal(false); | ||
const offlineAt = signal<number | undefined>(undefined); | ||
const onlineAt = signal<number | undefined>(undefined); | ||
const downlink = signal<number | undefined>(undefined); | ||
const downlinkMax = signal<number | undefined>(undefined); | ||
const rtt = signal<number | undefined>(undefined); | ||
const effectiveType = signal<NetworkEffectiveType | undefined>(undefined); | ||
const type = signal<NetworkType>('unknown'); | ||
|
||
const connection = supported() && (navigator as any).connection; | ||
|
||
const updateNetworkInformation = () => { | ||
if (!navigator) return; | ||
|
||
offlineAt.set(online() ? undefined : Date.now()); | ||
onlineAt.set(online() ? Date.now() : undefined); | ||
|
||
if (connection) { | ||
downlink.set(connection.downlink); | ||
downlinkMax.set(connection.downlinkMax); | ||
effectiveType.set(connection.effectiveType); | ||
rtt.set(connection.rtt); | ||
saveData.set(connection.saveData); | ||
type.set(connection.type); | ||
} | ||
}; | ||
|
||
if (window) { | ||
merge( | ||
fromEvent(window, 'online').pipe(map(() => true)), | ||
fromEvent(window, 'offline').pipe(map(() => false)) | ||
) | ||
.pipe(takeUntilDestroyed()) | ||
.subscribe((isOnline) => { | ||
online.set(isOnline); | ||
if (isOnline) { | ||
onlineAt.set(Date.now()); | ||
} else { | ||
offlineAt.set(Date.now()); | ||
} | ||
}); | ||
} | ||
|
||
if (connection) { | ||
fromEvent(connection, 'change') | ||
.pipe( | ||
startWith(null), // we need to start with null to trigger the first update | ||
takeUntilDestroyed() | ||
) | ||
.subscribe(() => updateNetworkInformation()); | ||
} | ||
|
||
return { | ||
supported: supported.asReadonly(), | ||
online: online.asReadonly(), | ||
saveData: saveData.asReadonly(), | ||
offlineAt: offlineAt.asReadonly(), | ||
onlineAt: onlineAt.asReadonly(), | ||
downlink: downlink.asReadonly(), | ||
downlinkMax: downlinkMax.asReadonly(), | ||
effectiveType: effectiveType.asReadonly(), | ||
rtt: rtt.asReadonly(), | ||
type: type.asReadonly(), | ||
}; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters