Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions docs/src/content/docs/utilities/Injectors/inject-element-bounding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
title: injectElementBounding
description: ngxtension/inject-element-bounding
entryPoint: ngxtension/inject-element-bounding
badge: stable
contributors: ['enea-jahollari']
---

Reactive bounding box of an HTML element. This utility tracks the position and dimensions of an element, automatically updating when the element is resized, moved, or when the window is scrolled or resized.

```ts
import { injectElementBounding } from 'ngxtension/inject-element-bounding';
```

## Usage

`injectElementBounding` accepts a signal that returns an `ElementRef` or `HTMLElement`, and optional configuration options. It returns an object containing reactive signals for all bounding box properties.

```ts
import { Component, ElementRef, viewChild } from '@angular/core';
import { injectElementBounding } from 'ngxtension/inject-element-bounding';

@Component({
selector: 'app-example',
standalone: true,
template: `
<div #target class="box">Resize or scroll to see bounding box updates</div>
<div class="info">
<p>Width: {{ bounding.width() }}px</p>
<p>Height: {{ bounding.height() }}px</p>
<p>Top: {{ bounding.top() }}px</p>
<p>Left: {{ bounding.left() }}px</p>
<p>Right: {{ bounding.right() }}px</p>
<p>Bottom: {{ bounding.bottom() }}px</p>
</div>
`,
})
export class ExampleComponent {
target = viewChild<ElementRef<HTMLDivElement>>('target');
bounding = injectElementBounding(this.target);
}
```

### With Options

```ts
@Component({
selector: 'app-example',
standalone: true,
template: `
<div #target>Content</div>
`,
})
export class ExampleComponent {
target = viewChild<ElementRef<HTMLDivElement>>('target');

bounding = injectElementBounding(this.target, {
reset: true, // Reset to 0 when element is removed
windowResize: true, // Listen to window resize events
windowScroll: true, // Listen to window scroll events
immediate: true, // Calculate immediately on mount
updateTiming: 'sync', // Update synchronously or on next frame
});

constructor() {
effect(() => {
console.log('Element width:', this.bounding.width());
console.log('Element position:', {
x: this.bounding.x(),
y: this.bounding.y(),
});
});
}
}
```

### With Raw HTMLElement

You can also use raw `HTMLElement` instead of `ElementRef`:

```ts
@Component({
selector: 'app-example',
standalone: true,
template: `
<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 {
  // ...
}

target = viewChild<ElementRef<HTMLDivElement>>('target');
elementSignal = signal<HTMLElement | null>(null);

bounding = injectElementBounding(this.elementSignal);

ngAfterViewInit() {
const el = this.target()?.nativeElement;
if (el) {
this.elementSignal.set(el);
}
}
}
```

### Manual Updates

The returned object includes an `update()` function for manual recalculation:

```ts
@Component({
selector: 'app-example',
standalone: true,
template: `
<div #target>Content</div>
<button (click)="refresh()">Refresh Bounding Box</button>
`,
})
export class ExampleComponent {
target = viewChild<ElementRef<HTMLDivElement>>('target');
bounding = injectElementBounding(this.target);

refresh() {
this.bounding.update();
}
}
```

## API

```ts
function injectElementBounding(
target: Signal<ElementRef<HTMLElement> | HTMLElement | null | undefined>,
options?: InjectElementBoundingOptions,
): InjectElementBoundingReturn;
```

### Parameters

- `target`: A signal that returns an `ElementRef`, `HTMLElement`, or `null/undefined`
- `options` (optional): Configuration object with the following properties:
- `injector`: An `Injector` instance for dependency injection
- `reset`: Reset values to 0 when element is removed (default: `true`)
- `windowResize`: Listen to window resize events (default: `true`)
- `windowScroll`: Listen to window scroll events (default: `true`)
- `immediate`: Calculate bounding box immediately on mount (default: `true`)
- `updateTiming`: When to recalculate - `'sync'` for immediate or `'next-frame'` for next animation frame (default: `'sync'`)

### Returns

An object with the following readonly signal properties:

- `height`: Signal containing the element's height in pixels
- `width`: Signal containing the element's width in pixels
- `top`: Signal containing the distance from the element's top edge to the viewport top
- `left`: Signal containing the distance from the element's left edge to the viewport left
- `right`: Signal containing the distance from the element's right edge to the viewport left
- `bottom`: Signal containing the distance from the element's bottom edge to the viewport top
- `x`: Signal containing the element's x coordinate (same as `left`)
- `y`: Signal containing the element's y coordinate (same as `top`)
- `update`: Function to manually trigger a recalculation of the bounding box

## How it Works

`injectElementBounding` uses several browser APIs to track element bounds:

1. **ResizeObserver**: Detects when the element's size changes
2. **MutationObserver**: Watches for changes to the element's `style` and `class` attributes
3. **Window Events**: Optionally listens to `scroll` and `resize` events on the window
4. **getBoundingClientRect()**: Calculates the actual bounding box values

All observers and event listeners are automatically cleaned up when the component is destroyed.

## Use Cases

- Creating tooltips or popovers that need to position relative to an element
- Implementing sticky headers or scroll-triggered animations
- Building responsive components that react to their own size changes
- Tracking element visibility and position for analytics
- Creating drag-and-drop interfaces with accurate collision detection
200 changes: 200 additions & 0 deletions docs/src/content/docs/utilities/Injectors/inject-element-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: injectElementSize
description: ngxtension/inject-element-size
entryPoint: ngxtension/inject-element-size
badge: stable
contributors: ['enea-jahollari']
---

Reactive size of an HTML element using ResizeObserver. This injector automatically tracks the width and height of an element and updates signals whenever the element is resized.

```ts
import { injectElementSize } from 'ngxtension/inject-element-size';
```

## Usage

`injectElementSize` accepts a target element reference (either an `ElementRef` or a `Signal<ElementRef>`) and optional configuration options. It returns an object containing readonly signals for `width` and `height` that automatically update when the element size changes.

### Basic Example

```ts
import { Component, ElementRef, viewChild } from '@angular/core';
import { injectElementSize } from 'ngxtension/inject-element-size';

@Component({
selector: 'app-element-size',
standalone: true,
template: `
<div
#resizableElement
style="resize: both; overflow: auto; width: 200px; height: 150px; border: 1px solid;"
>
Resize me!
</div>
<p>Width: {{ size.width() }}px</p>
<p>Height: {{ size.height() }}px</p>
`,
})
export class ElementSizeComponent {
resizableElement = viewChild<ElementRef>('resizableElement');
size = injectElementSize(this.resizableElement);
}
```

### With Initial Size

You can provide an initial size that will be used before the ResizeObserver initializes:

```ts
import { Component, ElementRef, viewChild } from '@angular/core';
import { injectElementSize } from 'ngxtension/inject-element-size';

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

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>
`,
})

export class ElementSizeComponent {
myElement = viewChild<ElementRef>('myElement');
size = injectElementSize(this.myElement, {
initialSize: { width: 100, height: 100 },
});
}
```

### Different Box Models

The `box` option allows you to specify which box model to use for measurements:

```ts
import { Component, ElementRef, viewChild } from '@angular/core';
import { injectElementSize } from 'ngxtension/inject-element-size';

@Component({
selector: 'app-element-size',
standalone: true,
template: `
<div #myElement style="padding: 20px; border: 5px solid;">Content</div>
<div>
<p>Content Box - Width: {{ contentBoxSize.width() }}px</p>
<p>Border Box - Width: {{ borderBoxSize.width() }}px</p>
</div>
`,
})
export class ElementSizeComponent {
myElement = viewChild<ElementRef>('myElement');

// Only measures the content area
contentBoxSize = injectElementSize(this.myElement, {
box: 'content-box',
});

// Includes padding and border
borderBoxSize = injectElementSize(this.myElement, {
box: 'border-box',
});
}
```

### Using with Dynamic Elements

When working with elements that may not be immediately available, you can pass a signal:

```ts
import { Component, ElementRef, signal } from '@angular/core';
import { injectElementSize } from 'ngxtension/inject-element-size';

@Component({
selector: 'app-dynamic-element',
standalone: true,
template: `
@if (showElement()) {
<div #dynamicElement>Dynamic Content</div>
}
<button (click)="toggleElement()">Toggle Element</button>
<p>Width: {{ size.width() }}px</p>
<p>Height: {{ size.height() }}px</p>
`,
})
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);
}
}
Comment on lines +123 to +134

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

```

### SVG Elements

The injector properly handles SVG elements by using `getBoundingClientRect()`:

```ts
import { Component, ElementRef, viewChild } from '@angular/core';
import { injectElementSize } from 'ngxtension/inject-element-size';

@Component({
selector: 'app-svg-size',
standalone: true,
template: `
<svg #svgElement width="200" height="100">
<rect width="100%" height="100%" fill="blue" />
</svg>
<p>SVG Width: {{ size.width() }}px</p>
<p>SVG Height: {{ size.height() }}px</p>
`,
})
export class SVGSizeComponent {
svgElement = viewChild<ElementRef>('svgElement');
size = injectElementSize(this.svgElement);
}
```

## API

```ts
function injectElementSize(
target: ElementRef<HTMLElement> | Signal<ElementRef<HTMLElement> | undefined>,
options?: InjectElementSizeOptions,
): Readonly<ElementSizeState>;
```

### Parameters

- `target`: The target element to observe. Can be:

- `ElementRef<HTMLElement>`: A static element reference
- `Signal<ElementRef<HTMLElement> | undefined>`: A signal containing an element reference (useful for dynamic elements)

- `options` (optional): An object that can have the following properties:
- `initialSize`: The initial size of the element (default: `{ width: 0, height: 0 }`)
- `box`: The box model to use for ResizeObserver (default: `'content-box'`)
- `'content-box'`: Only the content area
- `'border-box'`: Content + padding + border
- `'device-pixel-content-box'`: Content in device pixels
- `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:

- `width`: A readonly signal that emits the current width of the element in pixels
- `height`: A readonly signal that emits the current height of the element in pixels

## Notes

- The injector uses the native `ResizeObserver` API, which is supported in all modern browsers
- The signals are readonly to prevent external modifications
- The ResizeObserver is automatically cleaned up when the component is destroyed
- For SVG elements, the injector uses `getBoundingClientRect()` to get accurate dimensions
- When the target element is not available (undefined), the signals will use the `initialSize` values
Loading
Loading