Skip to content

Commit

Permalink
feat: added injectDocumentVisibility utility
Browse files Browse the repository at this point in the history
Co-authored-by: Enea Jahollari <jahollarienea14@gmail.com>
  • Loading branch information
fiorelozere and eneajaho committed Nov 30, 2023
1 parent 0275c02 commit f6b5e77
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 0 deletions.
3 changes: 3 additions & 0 deletions apps/test-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import { RouterLink, RouterOutlet } from '@angular/router';
<li>
<a routerLink="/active-element">Active Element</a>
</li>
<li>
<a routerLink="/document-visibility-state">Document Visibility State</a>
</li>
</ul>
<hr />
Expand Down
7 changes: 7 additions & 0 deletions apps/test-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export const appConfig: ApplicationConfig = {
loadComponent: () =>
import('./active-element/active-element.component'),
},
{
path: 'document-visibility-state',
loadComponent: () =>
import(
'./document-visibility-state/document-visibility-state.component'
),
},
]),
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component, effect } from '@angular/core';
import { injectDocumentVisibility } from 'ngxtension/document-visibility-state';

@Component({
template: `
{{ visibilityState() }}
`,
standalone: true,
})
export default class DocumentVisibilityStateComponent {
visibilityState = injectDocumentVisibility();

constructor() {
effect(() => {
console.log(this.visibilityState());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: documentVisibilityState
description: ngxtension/document-visibility-state
badge: stable
contributor: fiorelozere
---

`injectDocumentVisibility` is a utility function that provides a reactive signal reflecting the current visibility state of the document. This function is particularly useful for scenarios such as tracking user presence on a webpage (e.g., for analytics or pausing/resuming activities) and can be adapted for use with iframes or in testing environments.

```ts
import { injectDocumentVisibility } from 'ngxtension/document-visibility-state';
```

## Usage

`injectDocumentVisibility` accepts an optional parameter `options` which can include a custom `document` and an `Injector` instance. The `document` parameter is particularly useful for testing scenarios or when needing to track the visibility 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 visibilityState = injectDocumentVisibility();

effect(() => {
console.log(visibilityState.value);
});
```

## API

```ts
function injectDocumentVisibility(options?: InjectDocumentVisibilityOptions): Signal<DocumentVisibilityState>;
```

### Parameters

- `options` (optional): An object that can have the following properties:
- `document`: A custom `Document` instance, defaulting to the global `document` object.
- `injector`: An `Injector` instance for Angular's dependency injection.

### Returns

- `Signal<DocumentVisibilityState>`: A reactive signal that emits the current `DocumentVisibilityState` (e.g., `"visible"`, `"hidden"`) and updates when the document visibility state changes.
3 changes: 3 additions & 0 deletions libs/ngxtension/document-visibility-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/document-visibility-state

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/document-visibility-state`.
5 changes: 5 additions & 0 deletions libs/ngxtension/document-visibility-state/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/document-visibility-state/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/document-visibility-state",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/document-visibility-state/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["document-visibility-state"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/document-visibility-state/**/*.ts",
"libs/ngxtension/document-visibility-state/**/*.html"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { injectDocumentVisibility } from './document-visibility-state';

describe(injectDocumentVisibility.name, () => {
@Component({ standalone: true, template: '{{visibilityState()}}' })
class Test {
visibilityState = injectDocumentVisibility();
}

function setup() {
const fixture = TestBed.createComponent(Test);
fixture.detectChanges();
return fixture.componentInstance;
}

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);
}

it('should work properly', () => {
const cmp = setup();
triggerVisibilityChange('hidden');
expect(cmp.visibilityState()).toEqual('hidden');
triggerVisibilityChange('visible');
expect(cmp.visibilityState()).toEqual('visible');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DOCUMENT } from '@angular/common';
import {
Injector,
inject,
runInInjectionContext,
type Signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { assertInjector } from 'ngxtension/assert-injector';
import { Observable, fromEvent, map, startWith } from 'rxjs';

export interface InjectDocumentVisibilityOptions {
/*
* Specify a custom `document` instance, e.g. working with iframes or in testing environments.
*/
document?: Document;

/*
* Specify a custom `Injector` instance to use for dependency injection.
*/
injector?: Injector;
}

/**
* Injects and monitors the current document visibility state. Emits the state initially and then emits on every change.
*
* This function is useful for scenarios like tracking user presence on a page (e.g., for analytics or pausing/resuming activities) and is adaptable for use with iframes or in testing environments.
*
* @example
* ```ts
const visibilityState = injectDocumentVisibility();
effect(() => {
console.log(this.visibilityState());
});
* ```
*
* @param options An optional object with the following properties:
* - `document`: (Optional) Specifies a custom `Document` instance. This is useful when working with iframes or in testing environments where the global `document` 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 signal that emits the current `DocumentVisibilityState` (`"visible"`, `"hidden"`, etc.) initially and whenever the document visibility state changes.
*/

export function injectDocumentVisibility(
options?: InjectDocumentVisibilityOptions
): Signal<DocumentVisibilityState> {
const injector = assertInjector(injectDocumentVisibility, options?.injector);

return runInInjectionContext(injector, () => {
const doc: Document = options?.document ?? inject(DOCUMENT);

const docVisible$: Observable<DocumentVisibilityState> = fromEvent(
doc,
'visibilitychange'
).pipe(
startWith(doc.visibilityState),
map(() => doc.visibilityState)
);

return toSignal<DocumentVisibilityState>(docVisible$, {
requireSync: true,
});
});
}
1 change: 1 addition & 0 deletions libs/ngxtension/document-visibility-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './document-visibility-state';
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"libs/ngxtension/create-injection-token/src/index.ts"
],
"ngxtension/debug": ["libs/ngxtension/debug/src/index.ts"],
"ngxtension/document-visibility-state": [
"libs/ngxtension/document-visibility-state/src/index.ts"
],
"ngxtension/filter-array": ["libs/ngxtension/filter-array/src/index.ts"],
"ngxtension/filter-nil": ["libs/ngxtension/filter-nil/src/index.ts"],
"ngxtension/gestures": ["libs/ngxtension/gestures/src/index.ts"],
Expand Down

0 comments on commit f6b5e77

Please sign in to comment.