Skip to content

feat(utilities): add multiple injectors and documentation for enhance… - #680

Draft
eneajaho wants to merge 1 commit into
mainfrom
feat/vueuse-stuff
Draft

feat(utilities): add multiple injectors and documentation for enhance…#680
eneajaho wants to merge 1 commit into
mainfrom
feat/vueuse-stuff

Conversation

@eneajaho

Copy link
Copy Markdown
Collaborator

…d reactive features

Added new utilities including inject-bluetooth, inject-element-bounding, inject-element-visibility and others to the tsconfig.base.json. Comprehensive documentation was also added to provide guidance on their usage.

TODO

[ ] - Mention vueuse on all the utilties that were ported
[ ] - cleanup some utils and dx usage
[ ] - add more tests

…d reactive features

Added new utilities including `inject-bluetooth`, `inject-element-bounding`, `inject-element-visibility` and others to the `tsconfig.base.json`. Comprehensive documentation was also added to provide guidance on their usage.
@nx-cloud

nx-cloud Bot commented Mar 29, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 77d1d23

Command Status Duration Result
nx affected --target=lint --parallel=3 ❌ Failed 1m 12s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 4s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-29 20:48:08 UTC

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive suite of reactive injectors ported from VueUse, including utilities for Bluetooth, Clipboard, Color Mode, Element Bounding, Media Controls, and more. Each new utility is accompanied by documentation and unit tests. The review feedback identifies critical errors in documentation examples regarding the timing of viewChild access, suggests optimizing internal implementations by replacing polling with reactive effects, and recommends API enhancements for better consistency and support for dynamic content.

Comment on lines +68 to +73
export class ScrollTrackerComponent {
targetElement = viewChild.required<ElementRef>('target');
isVisible = injectElementVisibility({
element: this.targetElement().nativeElement,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This example is incorrect. viewChild.required results are not available during property initialization, so this.targetElement() will throw a runtime error. This incorrect pattern is used in several other examples in this file (ScrollContainerComponent, InfiniteScrollComponent, StickyNavComponent).

All these examples need to be corrected to initialize injectElementVisibility only after the view children are available (e.g., in ngAfterViewInit or by making the element option reactive).

Comment on lines +123 to +134
export class DynamicElementComponent {
showElement = signal(false);
elementRef = signal<ElementRef | undefined>(undefined);

size = injectElementSize(this.elementRef, {
initialSize: { width: 0, height: 0 },
});

toggleElement() {
this.showElement.update((v) => !v);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This example for dynamic elements is non-functional because the elementRef signal is declared but never updated with the element reference. To fix this, you can use viewChild to get the element and a computed signal to pass it to injectElementSize.

Also, signal, viewChild, and computed should be imported from @angular/core.

Suggested change
export class DynamicElementComponent {
showElement = signal(false);
elementRef = signal<ElementRef | undefined>(undefined);
size = injectElementSize(this.elementRef, {
initialSize: { width: 0, height: 0 },
});
toggleElement() {
this.showElement.update((v) => !v);
}
}
export class DynamicElementComponent {
showElement = signal(false);
dynamicElement = viewChild<ElementRef>('dynamicElement');
elementRef = computed(() => this.dynamicElement());
size = injectElementSize(this.elementRef, {
initialSize: { width: 0, height: 0 },
});
toggleElement() {
this.showElement.update((v) => !v);
}
}

<div #target>Content</div>
`,
})
export class ExampleComponent {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This example uses signal and the ngAfterViewInit lifecycle hook, but the code snippet doesn't show the import for signal or the class implementing AfterViewInit. For clarity and correctness, please update the example to include these.

import { Component, ElementRef, signal, viewChild, AfterViewInit } from '@angular/core';

// ...

export class ExampleComponent implements AfterViewInit {
  // ...
}

Comment on lines +53 to +60
@Component({
selector: 'app-element-size',
template: `
<div #myElement>Content</div>
<p>Width: {{ size.width() }}px</p>
<p>Height: {{ size.height() }}px</p>
`,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The @Component decorator is missing standalone: true. All other component examples in this document are standalone, so this seems to be an omission that would prevent the code from working as-is.

Suggested change
@Component({
selector: 'app-element-size',
template: `
<div #myElement>Content</div>
<p>Width: {{ size.width() }}px</p>
<p>Height: {{ size.height() }}px</p>
`,
})
@Component({
selector: 'app-element-size',
standalone: true,
template: `
<div #myElement>Content</div>
<p>Width: {{ size.width() }}px</p>
<p>Height: {{ size.height() }}px</p>
`,
})


`injectElementVisibility` tracks whether an element is visible in the viewport and returns a signal that updates whenever the visibility state changes.

### Basic usage with ElementRef

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The heading "Basic usage with ElementRef" is misleading as the example below demonstrates usage with the host element, not a specific ElementRef.

Suggested change
### Basic usage with ElementRef
### Basic usage with the host element


// Check if Clipboard API with ClipboardItem is supported
const isSupported = computed(
() => navigator != null && 'clipboard' in navigator,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The isSupported check only verifies navigator.clipboard. Since this utility is specifically for ClipboardItem, it would be more accurate to also check for the existence of ClipboardItem in the global scope to ensure full functionality is available.

Suggested change
() => navigator != null && 'clipboard' in navigator,
() => navigator != null && 'clipboard' in navigator && typeof ClipboardItem !== 'undefined',

Comment on lines +168 to +205
if (typeof target === 'function') {
// Watch for element changes
new Observable<HTMLElement | null>((subscriber) => {
const checkElement = () => {
const element = getElement();
subscriber.next(element);
};

// Initial check
checkElement();

// Use effect to watch signal changes - but we need to do this outside Observable
// So we'll use a different approach with an interval or manual tracking
const interval = setInterval(checkElement, 100);

return () => {
clearInterval(interval);
};
})
.pipe(takeUntilDestroyed())
.subscribe((element) => {
// Disconnect previous observations
resizeObserver.disconnect();

if (element) {
// Update to current size immediately
if ('offsetWidth' in element) {
width.set((element as HTMLElement).offsetWidth);
}
if ('offsetHeight' in element) {
height.set((element as HTMLElement).offsetHeight);
}
resizeObserver.observe(element, { box });
} else {
width.set(initialSize.width);
height.set(initialSize.height);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the target is a signal, the current implementation uses setInterval for polling, which is inefficient. Since injectElementSize runs within an injection context, you can use an effect to react to changes in the target signal. This is more efficient and idiomatic for Angular Signals.

			if (typeof target === 'function') {
				// Watch for element changes with an effect
				effect((onCleanup) => {
					const element = getElement();
					if (element) {
						resizeObserver.observe(element, { box });
					} else {
						width.set(initialSize.width);
						height.set(initialSize.height);
					}

					onCleanup(() => resizeObserver.disconnect());
				});
			}

Comment on lines +14 to +54
export interface InjectElementVisibilityOptions {
/**
* Custom injector instance for dependency injection.
*/
injector?: Injector;
/**
* The element to track visibility for. If not provided, will attempt to inject ElementRef.
*/
element?: Element | ElementRef<Element>;
/**
* Custom window instance. Useful for testing or iframe scenarios.
*/
window?: Window;
/**
* Initial value for visibility.
*
* @default false
*/
initialValue?: boolean;
/**
* The element that is used as the viewport for checking visibility of the target.
*/
scrollTarget?: Element | null;
/**
* Margin around the root. Can have values similar to the CSS margin property.
*/
rootMargin?: string;
/**
* Either a single number or an array of numbers which indicate at what percentage
* of the target's visibility the observer's callback should be executed.
*
* @default 0
*/
threshold?: number | number[];
/**
* Stop tracking when element visibility changes for the first time
*
* @default false
*/
once?: boolean;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The element option in InjectElementVisibilityOptions only accepts Element | ElementRef<Element>, but not a Signal. This makes it difficult to use with elements that are not immediately available (e.g., inside an @if block), and is inconsistent with other injectors in ngxtension like injectElementBounding.

Consider updating this injector to also accept a Signal<Element | ElementRef<Element> | undefined> for the element option to improve its usability with dynamic content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant