diff --git a/docs/src/content/contributors/fabiendehopre.json b/docs/src/content/contributors/fabiendehopre.json
new file mode 100644
index 00000000..497a6ef1
--- /dev/null
+++ b/docs/src/content/contributors/fabiendehopre.json
@@ -0,0 +1,6 @@
+{
+ "name": "Fabien Dehopré",
+ "twitter": "https://twitter.com/FabienDehopre",
+ "github": "https://github.com/FabienDehopre",
+ "linkedin": "https://www.linkedin.com/in/fabien1979/"
+}
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index 1b4c657c..abe61f31 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -65,8 +65,10 @@ import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
- [filterNil - filter out null and undefined values](/utilities/operators/filter-nil/)
- [mapArray - map array emitted by the stream](/utilities/operators/map-array/)
- [mapSkipUndefined - skip undefined on map](/utilities/operators/map-skip-undefined/)
+ - [poll - poll a stream every "period" milliseconds](/utilities/operators/poll/)
- [reduceArray - reduce array emitted by the stream](/utilities/operators/reduce-array/)
- [rxEffect - utility to create a side effect with rxjs](/utilities/operators/rx-effect/)
+ - [whenDocumentVisible - pause a stream when the document is hidden](/utilities/operators/when-document-visible/)
diff --git a/docs/src/content/docs/utilities/Operators/poll.md b/docs/src/content/docs/utilities/Operators/poll.md
new file mode 100644
index 00000000..b1ccebcb
--- /dev/null
+++ b/docs/src/content/docs/utilities/Operators/poll.md
@@ -0,0 +1,31 @@
+---
+title: poll
+description: RxJS operator to apply to a stream that you want to poll every "period" milliseconds after an optional "initialDelay" milliseconds.
+entryPoint: poll
+badge: stable
+contributors: ['fabiendehopre']
+---
+
+## Import
+
+```typescript
+import { poll } from 'ngxtension/poll';
+```
+
+## Usage
+
+You can use this operator to poll and API at a fixed interval with an optional initial delay.
+
+```typescript
+import { HttpClient } from '@angular/common/http';
+import { inject } from '@angular/core';
+import { poll } from 'ngxtension/poll';
+
+const httpClient = inject(HttpClient);
+httpClient
+ .get('https://api.example.com/data')
+ .pipe(
+ poll(10000, 5000), // poll every 10s after 5s
+ )
+ .subscribe(console.log);
+```
diff --git a/docs/src/content/docs/utilities/Operators/when-document-visible.md b/docs/src/content/docs/utilities/Operators/when-document-visible.md
new file mode 100644
index 00000000..ca340930
--- /dev/null
+++ b/docs/src/content/docs/utilities/Operators/when-document-visible.md
@@ -0,0 +1,39 @@
+---
+title: whenDocumentVisible
+description: RxJS operator to pause a stream when the document is hidden and to resume the stream when the document is visible.
+entryPoint: when-document-visible
+badge: stable
+contributors: ['fabiendehopre']
+---
+
+## Import
+
+```typescript
+import { whenDocumentVisible } from 'ngxtension/when-document-visible';
+```
+
+## Usage
+
+You can use it to pause a stream when the document is hidden and to resume the stream when the document is visible.
+
+It uses the same options as the `injectDocumentVisiblity` function.
+
+A good use case is to pause an API polling when the user switches to another tab or another application.
+
+```typescript
+import { DOCUMENT } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { inject } from '@angular/core';
+import { poll } from 'ngxtension/poll';
+import { whenDocumentVisible } from 'ngxtension/when-document-visible';
+
+const httpClient = inject(HttpClient);
+const document = inject(DOCUMENT);
+httpClient
+ .get('https://api.example.com/data')
+ .pipe(
+ poll(10000, 5000), // poll every 10s after 5s
+ whenDocumentVisible({ document }),
+ )
+ .subscribe(console.log);
+```
diff --git a/libs/ngxtension/inject-document-visibility/src/inject-document-visibility.ts b/libs/ngxtension/inject-document-visibility/src/inject-document-visibility.ts
index 81859f03..856503c8 100644
--- a/libs/ngxtension/inject-document-visibility/src/inject-document-visibility.ts
+++ b/libs/ngxtension/inject-document-visibility/src/inject-document-visibility.ts
@@ -21,6 +21,24 @@ export interface InjectDocumentVisibilityOptions {
injector?: Injector;
}
+export function injectDocumentVisibilityStream(
+ options?: InjectDocumentVisibilityOptions,
+): Observable {
+ const injector = assertInjector(
+ injectDocumentVisibilityStream,
+ options?.injector,
+ );
+
+ return runInInjectionContext(injector, () => {
+ const doc: Document = options?.document ?? inject(DOCUMENT);
+
+ return fromEvent(doc, 'visibilitychange').pipe(
+ startWith(doc.visibilityState),
+ map(() => doc.visibilityState),
+ );
+ });
+}
+
/**
* Injects and monitors the current document visibility state. Emits the state initially and then emits on every change.
*
@@ -40,25 +58,12 @@ export interface InjectDocumentVisibilityOptions {
*
* @returns A signal that emits the current `DocumentVisibilityState` (`"visible"`, `"hidden"`, etc.) initially and whenever the document visibility state changes.
*/
-
export function injectDocumentVisibility(
options?: InjectDocumentVisibilityOptions,
): Signal {
- const injector = assertInjector(injectDocumentVisibility, options?.injector);
-
- return runInInjectionContext(injector, () => {
- const doc: Document = options?.document ?? inject(DOCUMENT);
-
- const docVisible$: Observable = fromEvent(
- doc,
- 'visibilitychange',
- ).pipe(
- startWith(doc.visibilityState),
- map(() => doc.visibilityState),
- );
-
- return toSignal(docVisible$, {
- requireSync: true,
- });
+ const docVisible$: Observable =
+ injectDocumentVisibilityStream(options);
+ return toSignal(docVisible$, {
+ requireSync: true,
});
}
diff --git a/libs/ngxtension/poll/README.md b/libs/ngxtension/poll/README.md
new file mode 100644
index 00000000..0ae0eaa2
--- /dev/null
+++ b/libs/ngxtension/poll/README.md
@@ -0,0 +1,3 @@
+# ngxtension/poll
+
+Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/poll`.
diff --git a/libs/ngxtension/poll/ng-package.json b/libs/ngxtension/poll/ng-package.json
new file mode 100644
index 00000000..b3e53d69
--- /dev/null
+++ b/libs/ngxtension/poll/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/ngxtension/poll/project.json b/libs/ngxtension/poll/project.json
new file mode 100644
index 00000000..b85cfb51
--- /dev/null
+++ b/libs/ngxtension/poll/project.json
@@ -0,0 +1,27 @@
+{
+ "name": "ngxtension/poll",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "libs/ngxtension/poll/src",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/ngxtension/jest.config.ts",
+ "testPathPattern": ["poll"],
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "outputs": ["{options.outputFile}"]
+ }
+ }
+}
diff --git a/libs/ngxtension/poll/src/index.ts b/libs/ngxtension/poll/src/index.ts
new file mode 100644
index 00000000..3504df2e
--- /dev/null
+++ b/libs/ngxtension/poll/src/index.ts
@@ -0,0 +1 @@
+export { poll } from './poll';
diff --git a/libs/ngxtension/poll/src/poll.spec.ts b/libs/ngxtension/poll/src/poll.spec.ts
new file mode 100644
index 00000000..f29e6f32
--- /dev/null
+++ b/libs/ngxtension/poll/src/poll.spec.ts
@@ -0,0 +1,41 @@
+import { fakeAsync, tick } from '@angular/core/testing';
+import { of } from 'rxjs';
+import { poll } from './poll';
+
+describe(poll.name, () => {
+ it('should return an observable that polls every 1000ms', fakeAsync(() => {
+ const callback = jest.fn();
+ const source = of('test');
+ const result = source.pipe(poll(1000));
+ const sub = result.subscribe(() => {
+ callback();
+ });
+ expect(callback).toHaveBeenCalledTimes(0);
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(2);
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(3);
+ sub.unsubscribe();
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(3);
+ }));
+
+ it('should return an observable that polls every 1000ms after 500ms', fakeAsync(() => {
+ const callback = jest.fn();
+ const source = of('test');
+ const result = source.pipe(poll(1000, 500));
+ const sub = result.subscribe(() => {
+ callback();
+ });
+ expect(callback).toHaveBeenCalledTimes(0);
+ tick(500);
+ expect(callback).toHaveBeenCalledTimes(1);
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(2);
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(3);
+ sub.unsubscribe();
+ tick(1000);
+ expect(callback).toHaveBeenCalledTimes(3);
+ }));
+});
diff --git a/libs/ngxtension/poll/src/poll.ts b/libs/ngxtension/poll/src/poll.ts
new file mode 100644
index 00000000..0e679bec
--- /dev/null
+++ b/libs/ngxtension/poll/src/poll.ts
@@ -0,0 +1,23 @@
+import { concatMap, MonoTypeOperatorFunction, Observable, timer } from 'rxjs';
+// source: https://netbasal.com/use-rxjs-to-modify-app-behavior-based-on-page-visibility-ce499c522be4
+
+/**
+ * RxJS operator to apply to a stream that you want to poll every "period" milliseconds after an optional "initialDelay" milliseconds.
+ *
+ * @param period - Indicates the delay between 2 polls.
+ * @param initialDelay - Indicates the delay before the first poll occurs.
+ * @example This example shows how to do an HTTP GET every 5 seconds:
+ * ```ts
+ * http.get('http://api').pipe(poll(5000)).subscribe(result => {});
+ * ```
+ * @public
+ */
+export function poll(
+ period: number,
+ initialDelay = 0,
+): MonoTypeOperatorFunction {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ return (source: Observable) => {
+ return timer(initialDelay, period).pipe(concatMap(() => source));
+ };
+}
diff --git a/libs/ngxtension/when-document-visible/README.md b/libs/ngxtension/when-document-visible/README.md
new file mode 100644
index 00000000..94a4534b
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/README.md
@@ -0,0 +1,3 @@
+# ngxtension/when-document-visible
+
+Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/when-document-visible`.
diff --git a/libs/ngxtension/when-document-visible/ng-package.json b/libs/ngxtension/when-document-visible/ng-package.json
new file mode 100644
index 00000000..b3e53d69
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/ngxtension/when-document-visible/project.json b/libs/ngxtension/when-document-visible/project.json
new file mode 100644
index 00000000..38623475
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/project.json
@@ -0,0 +1,27 @@
+{
+ "name": "ngxtension/when-document-visible",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "libs/ngxtension/when-document-visible/src",
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/ngxtension/jest.config.ts",
+ "testPathPattern": ["when-document-visible"],
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "outputs": ["{options.outputFile}"]
+ }
+ }
+}
diff --git a/libs/ngxtension/when-document-visible/src/index.ts b/libs/ngxtension/when-document-visible/src/index.ts
new file mode 100644
index 00000000..59d806d6
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/src/index.ts
@@ -0,0 +1 @@
+export const greeting = 'Hello World!';
diff --git a/libs/ngxtension/when-document-visible/src/when-document-visible.spec.ts b/libs/ngxtension/when-document-visible/src/when-document-visible.spec.ts
new file mode 100644
index 00000000..b4cb79b4
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/src/when-document-visible.spec.ts
@@ -0,0 +1,61 @@
+import { Injector } from '@angular/core';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Observable, timer } from 'rxjs';
+import { whenDocumentVisible } from './when-document-visible';
+
+describe(whenDocumentVisible.name, () => {
+ function triggerVisibilityChange(newState: DocumentVisibilityState) {
+ // Change the visibility state
+ Object.defineProperty(document, 'visibilityState', {
+ writable: true,
+ configurable: true,
+ value: newState,
+ });
+
+ // Dispatch the event
+ const event = new Event('visibilitychange');
+ document.dispatchEvent(event);
+ }
+
+ describe('should emit values only when document is visible', () => {
+ const runTest = (source: Observable): void => {
+ const callback = jest.fn();
+ triggerVisibilityChange('visible');
+ const sub = source.subscribe(callback);
+ tick(); // trigger initial emission
+ expect(callback).toHaveBeenCalledTimes(1);
+ tick(10_000); // will emit 10 times in 10 seconds
+ expect(callback).toHaveBeenCalledTimes(11);
+ triggerVisibilityChange('hidden');
+ tick(10_000);
+ expect(callback).toHaveBeenCalledTimes(11);
+ triggerVisibilityChange('visible');
+ tick(10_000); // will emit 11 times (once initially and 10 times in 10 seconds)
+ expect(callback).toHaveBeenCalledTimes(22);
+ sub.unsubscribe();
+ };
+
+ it('should emit values only when document is visible using provided injector', fakeAsync(() => {
+ const source = timer(0, 1000);
+ runTest(
+ source.pipe(
+ whenDocumentVisible({ injector: TestBed.inject(Injector) }),
+ ),
+ );
+ }));
+
+ it('should emit values only when document is visible in injector context', fakeAsync(() => {
+ TestBed.runInInjectionContext(() => {
+ const source = timer(0, 1000);
+ runTest(source.pipe(whenDocumentVisible()));
+ });
+ }));
+
+ it('should emit values only when document is visible using provided document', fakeAsync(() => {
+ TestBed.runInInjectionContext(() => {
+ const source = timer(0, 1000);
+ runTest(source.pipe(whenDocumentVisible({ document })));
+ });
+ }));
+ });
+});
diff --git a/libs/ngxtension/when-document-visible/src/when-document-visible.ts b/libs/ngxtension/when-document-visible/src/when-document-visible.ts
new file mode 100644
index 00000000..314392fa
--- /dev/null
+++ b/libs/ngxtension/when-document-visible/src/when-document-visible.ts
@@ -0,0 +1,36 @@
+import {
+ InjectDocumentVisibilityOptions,
+ injectDocumentVisibilityStream,
+} from 'ngxtension/inject-document-visibility';
+import {
+ MonoTypeOperatorFunction,
+ Observable,
+ partition,
+ repeat,
+ takeUntil,
+} from 'rxjs';
+
+/**
+ * RxJS operator to pause a stream when the document is hidden (i.e.: tab is not active)
+ * and to resume the stream when the document is visible (i.e.: tab is active).
+ *
+ * @example This example shows how to do an HTTP GET every 5 seconds only when the page is visible in the browser:
+ * ```typescript
+ * http.get('http://api').pipe(poll(5000), whenPageVisible()).subscribe(result => {})
+ * ```
+ * @public
+ */
+export function whenDocumentVisible(
+ options?: InjectDocumentVisibilityOptions,
+): MonoTypeOperatorFunction {
+ const visibilityChanged$ = injectDocumentVisibilityStream(options);
+ const [pageVisible$, pageHidden$] = partition(visibilityChanged$, () => {
+ return document.visibilityState === 'visible';
+ });
+ return (source: Observable) => {
+ return source.pipe(
+ takeUntil(pageHidden$),
+ repeat({ delay: () => pageVisible$ }),
+ );
+ };
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 8d9f4f8e..53c18c5c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -110,6 +110,7 @@
"ngxtension/navigation-end": [
"libs/ngxtension/navigation-end/src/index.ts"
],
+ "ngxtension/poll": ["libs/ngxtension/poll/src/index.ts"],
"ngxtension/repeat": ["libs/ngxtension/repeat/src/index.ts"],
"ngxtension/repeat-pipe": ["libs/ngxtension/repeat-pipe/src/index.ts"],
"ngxtension/resize": ["libs/ngxtension/resize/src/index.ts"],
@@ -128,6 +129,9 @@
"ngxtension/trackby-id-prop": [
"libs/ngxtension/trackby-id-prop/src/index.ts"
],
+ "ngxtension/when-document-visible": [
+ "libs/ngxtension/when-document-visible/src/index.ts"
+ ],
"plugin": ["libs/plugin/src/index.ts"]
}
},