Skip to content

Commit

Permalink
feat(components/dimension): provide signal to observe element size
Browse files Browse the repository at this point in the history
```ts
import {Component, effect, ElementRef, inject} from '@angular/core';
import {dimension} from '@scion/components/dimension';

@component({...})
class YourComponent {

  private host = inject(ElementRef<HTMLElement>);
  private dimension = dimension(this.host);

  constructor() {
    effect(() => console.log('size', this.dimension()));
  }
}
```
  • Loading branch information
danielwiehl committed Oct 13, 2024
1 parent c1fef65 commit 81a8364
Show file tree
Hide file tree
Showing 8 changed files with 691 additions and 66 deletions.
2 changes: 1 addition & 1 deletion docs/site/scion-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Provides Angular-based components and directives with a focus on SCION requireme
You can choose between different presentations: `ellipsis`, `ripple`, `roller`, `spinner`.

- [**Dimension**][link-tool-dimension]\
Provides a directive for observing changes in the size of the host element.
Provides a set of tools for observing the size of an element.

- [**SCION Design Tokens**][link-scion-design-tokens]\
SCION provides a set of design tokens to enable consistent design and theming of SCION components.
Expand Down
106 changes: 75 additions & 31 deletions docs/site/tools/dimension.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@

## [SCION Toolkit][menu-home] > [@scion/components][link-scion-components] > Dimension

The NPM sub-module `@scion/components/dimension` provides an Angular directive for observing the size of an HTML element. The directive emits the element's initial size, and then continuously emits when its size changes. It never completes.
The NPM sub-module `@scion/components/dimension` provides a set of tools for observing the size of an element.

Install the NPM module `@scion/components` as following:

```
npm install @scion/components @scion/toolkit @angular/cdk
```

<details>
<summary><strong>Installation and Usage</strong></summary>
<summary><strong>Dimension Directive</strong></summary>

1. Install `@scion/components` using the NPM command-line tool:
```
npm install @scion/components @scion/toolkit @angular/cdk
```
Directive to observe the size of an element in the HTML template.

1. Import `SciDimensionDirective` in your component.
1. Import `SciDimensionDirective`.

```typescript
```ts
import {SciDimensionDirective} from '@scion/components/dimension';

@Component({
Expand All @@ -29,40 +32,81 @@ The NPM sub-module `@scion/components/dimension` provides an Angular directive f
}
```

Alternatively, import `SciDimensionModule` in the `NgModule` that declares your component.

```typescript
import {SciDimensionModule} from '@scion/components/dimension';

@NgModule({
imports: [SciDimensionModule]
})
export class AppModule {
}
```

1. Add the `sciDimension` directive to the HTML element for which you want to observe its size:
1. Add `sciDimension` directive to an element in the template.

```html
<div sciDimension (sciDimensionChange)="onDimensionChange($event)"></div>
```

1. Add the following method to the component:
```typescript
public onDimensionChange(dimension: Dimension): void {
1. Add method to be notified about size changes of the element.
```ts
public onDimensionChange(dimension: SciDimension): void {
console.log(dimension);
}
```

The directive can be configured with `emitOutsideAngular` to control whether to emit inside or outside the Angular zone. Defaults to `false`.

</details>

<details>
<summary><strong>Control if to emit outside of the Angular zone</strong></summary>

You can control if to emit a dimension change inside or outside of the Angular zone by passing a `boolean` value to the input parameter `emitOutsideAngular`. If emitting outside of the Angular zone, the directive does not trigger an Angular change detection cycle. By default, dimension changes are emitted inside of the Angular zone.

```html
<div sciDimension (sciDimensionChange)="onDimensionChange($event)" [emitOutsideAngular]="false"></div>
```
<summary><strong>Dimension Signal</strong></summary>

Signal to observe the size of an element.

The signal uses to the native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to monitor element size changes. Destroying the injection context will unsubscribe from {@link ResizeObserver}.


**Usage:**
```ts
import {dimension} from '@scion/components/dimension';

const element: HTMLElement = ...;
const size = dimension(element);

console.log('size', size());
```

> The function must be called within an injection context or with an injector provided for automatic unsubscription.
> The function must NOT be called within a reactive context to avoid repeated subscriptions.
**Example of observing the size of a component:**

```ts
import {Component, effect, ElementRef, inject} from '@angular/core';
import {dimension} from '@scion/components/dimension';

@Component({...})
class YourComponent {

private host = inject(ElementRef<HTMLElement>);
private dimension = dimension(this.host);

constructor() {
effect(() => console.log('size', this.dimension()));
}
}
```

**Example of observing the size of a view child:**

The element can be passed as a signal, useful for observing a view child as view children cannot be read in the constructor.

```ts
import {Component, effect, ElementRef, viewChild} from '@angular/core';
import {dimension} from '@scion/components/dimension';

@Component({...})
class YourComponent {
private viewChild = viewChild<ElementRef<HTMLElement>>('view_child');
private dimension = dimension(this.viewChild);

constructor() {
effect(() => console.log('size', this.dimension()));
}
}
```

</details>

[menu-home]: /README.md
Expand Down
4 changes: 2 additions & 2 deletions projects/scion/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The SCION Components is a collection of Angular components and directives primar
Provides a viewport component with scrollbars that sit on top of the viewport client.

- [**Dimension**][link-tool-dimension]\
Provides a directive for observing changes in the size of the host element.
Provides a set of tools for observing the size of an element.

- [**Sashbox**][link-tool-sashbox]\
Provides a sashbox component for splitting content into multiple parts, which the user can resize by moving the splitter between the parts.
Expand Down Expand Up @@ -37,4 +37,4 @@ License: EPL-2.0
[link-tool-dimension]: https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/dimension.md
[link-tool-sashbox]: https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/sashbox.md
[link-tool-splitter]: https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/splitter.md
[link-tool-throbber]: https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/throbber.md
[link-tool-throbber]: https://github.com/SchweizerischeBundesbahnen/scion-toolkit/blob/master/docs/site/tools/throbber.md
20 changes: 4 additions & 16 deletions projects/scion/components/dimension/src/dimension.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import {fromResize$} from '@scion/toolkit/observable';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {observeInside, subscribeInside} from '@scion/toolkit/operators';
import {animationFrameScheduler, observeOn, subscribeOn} from 'rxjs';
import {SciDimension} from './dimension';

/**
* Observes changes to the size of the host element.
*
* Upon subscription, emits the current size, and then continuously when the size changes. The Observable never completes.
* Observes the size of the host element.
*
* ---
* Usage:
Expand All @@ -33,12 +32,12 @@ import {animationFrameScheduler, observeOn, subscribeOn} from 'rxjs';
export class SciDimensionDirective {

/**
* Controls if to emit outside the Angular zone. Defaults to `false`.
* Controls if to output outside the Angular zone. Defaults to `false`.
*/
public emitOutsideAngular = input(false);

/**
* Upon subscription, emits the current size, and then continuously when the size changes. The Observable never completes.
* Outputs the size of the element.
*/
public dimensionChange = output<SciDimension>({alias: 'sciDimensionChange'});

Expand All @@ -65,14 +64,3 @@ export class SciDimensionDirective {
});
}
}

/**
* Represents the dimension of an element.
*/
export interface SciDimension {
offsetWidth: number;
offsetHeight: number;
clientWidth: number;
clientHeight: number;
element: HTMLElement;
}
70 changes: 70 additions & 0 deletions projects/scion/components/dimension/src/dimension.signal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2018-2024 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import {assertInInjectionContext, assertNotInReactiveContext, computed, DestroyRef, effect, ElementRef, inject, Injector, isSignal, signal, Signal} from '@angular/core';
import {SciDimension} from './dimension';
import {coerceElement} from '@angular/cdk/coercion';
import {Objects} from '@scion/toolkit/util';

/**
* Creates a signal to observe the size of an element.
*
* The element can be passed as a signal, useful for observing a view child as view children cannot be read in the constructor.
*
* The signal uses to the native {@link ResizeObserver} to monitor element size changes. Destroying the injection context will unsubscribe from {@link ResizeObserver}.
*
* Usage:
* - Must be called within an injection context or with an injector provided for automatic unsubscription.
* - Must NOT be called within a reactive context to avoid repeated subscriptions.
*/
export function dimension(elementLike: HTMLElement | ElementRef<HTMLElement> | Signal<HTMLElement | ElementRef<HTMLElement>>, options?: {injector?: Injector}): Signal<SciDimension>;
export function dimension(elementLike: HTMLElement | ElementRef<HTMLElement> | Signal<HTMLElement | ElementRef<HTMLElement> | undefined>, options?: {injector?: Injector}): Signal<SciDimension | undefined>;
export function dimension(elementLike: HTMLElement | ElementRef<HTMLElement> | Signal<HTMLElement | ElementRef<HTMLElement> | undefined>, options?: {injector?: Injector}): Signal<SciDimension | undefined> {
assertNotInReactiveContext(dimension, 'Invoking `dimension` causes new subscriptions every time. Move `dimension` outside of the reactive context and read the signal value where needed.');
if (!options?.injector) {
assertInInjectionContext(dimension);
}

const injector = options?.injector ?? inject(Injector);
const element = computed(() => coerceElement(isSignal(elementLike) ? elementLike() : elementLike));
const onResize = signal<void>(undefined, {equal: () => false});

// Signal 'onResize' when the element size changes.
// Note: Run callback in animation frame to avoid the error: "ResizeObserver loop completed with undelivered notifications".
const resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => onResize.set()));
injector.get(DestroyRef).onDestroy(() => resizeObserver.disconnect());

// Connnect to the element.
effect(onCleanup => {
const el = element();
if (el) {
resizeObserver.observe(el);
onCleanup(() => resizeObserver.unobserve(el));
}
}, {injector});

return computed(() => {
const el = element();
if (!el) {
return undefined;
}

// Track when the element is resized.
onResize();

return {
clientWidth: el.clientWidth,
offsetWidth: el.offsetWidth,
clientHeight: el.clientHeight,
offsetHeight: el.offsetHeight,
element: el,
};
}, {equal: Objects.isEqual});
}
Loading

0 comments on commit 81a8364

Please sign in to comment.