diff --git a/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.html b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.html
new file mode 100644
index 00000000..d5c20e91
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.html
@@ -0,0 +1,20 @@
+
+
+
diff --git a/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.scss b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.scss
new file mode 100644
index 00000000..edee87a0
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.scss
@@ -0,0 +1,30 @@
+:host {
+ > div.testee {
+ position: absolute;
+ background-color: blue;
+ }
+
+ > aside {
+ position: fixed;
+ top: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2em;
+ padding: 1em;
+
+ > section {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: .25em 1em;
+
+ > header {
+ font-weight: bold;
+ }
+
+ > header, > button.apply {
+ grid-column: 1/-1;
+ }
+ }
+ }
+}
diff --git a/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.ts b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.ts
new file mode 100644
index 00000000..1a138b95
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/bounding-client-rect-page.component.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 {Component, DoCheck, effect, ElementRef, inject, NgZone, OnInit, signal, viewChild} from '@angular/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {boundingClientRect} from '@scion/components/dimension';
+
+@Component({
+ selector: 'app-bounding-client-rect-page',
+ templateUrl: './dimension-page.component.html',
+ styleUrl: './bounding-client-rect-page.component.scss',
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+})
+export class BoundingClientRectPageComponent implements OnInit, DoCheck {
+
+ private _testee = viewChild.required>('testee');
+ private _boundingBox = boundingClientRect(this._testee);
+ private applyButton = viewChild('apply_button', {read: ElementRef});
+ private zone = inject(NgZone);
+
+ protected properties = {
+ x: signal('0px'),
+ y: signal('0px'),
+ width: signal('100px'),
+ height: signal('100px'),
+ };
+
+ protected testeeBoundingBox = {
+ x: signal(undefined),
+ y: signal(undefined),
+ width: signal(undefined),
+ height: signal(undefined),
+ };
+
+ constructor() {
+ effect(() => {
+ const boundingBox = this._boundingBox();
+ this.testeeBoundingBox.x.set(boundingBox.x);
+ this.testeeBoundingBox.y.set(boundingBox.y);
+ this.testeeBoundingBox.width.set(boundingBox.width);
+ this.testeeBoundingBox.height.set(boundingBox.height);
+ });
+ }
+
+ public ngOnInit(): void {
+ this.applyProperties();
+ this.zone.runOutsideAngular(() => {
+ this.applyButton()!.nativeElement.addEventListener('click', () => this.applyProperties());
+ });
+ }
+
+ public ngDoCheck(): void {
+ console.log('[BoundingClientRectPageComponent] Angular change detection cycle');
+ }
+
+ public applyProperties(): void {
+ this._testee().nativeElement.style.left = this.properties.x();
+ this._testee().nativeElement.style.top = this.properties.y();
+ this._testee().nativeElement.style.width = this.properties.width();
+ this._testee().nativeElement.style.height = this.properties.height();
+ }
+}
diff --git a/apps/components-testing-app/src/app/components/dimension/dimension-page.component.html b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.html
new file mode 100644
index 00000000..d5c20e91
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.html
@@ -0,0 +1,20 @@
+
+
+
diff --git a/apps/components-testing-app/src/app/components/dimension/dimension-page.component.scss b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.scss
new file mode 100644
index 00000000..edee87a0
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.scss
@@ -0,0 +1,30 @@
+:host {
+ > div.testee {
+ position: absolute;
+ background-color: blue;
+ }
+
+ > aside {
+ position: fixed;
+ top: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2em;
+ padding: 1em;
+
+ > section {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: .25em 1em;
+
+ > header {
+ font-weight: bold;
+ }
+
+ > header, > button.apply {
+ grid-column: 1/-1;
+ }
+ }
+ }
+}
diff --git a/apps/components-testing-app/src/app/components/dimension/dimension-page.component.ts b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.ts
new file mode 100644
index 00000000..dc84bcdc
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/dimension-page.component.ts
@@ -0,0 +1,73 @@
+/*
+ * 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 {ChangeDetectionStrategy, Component, DoCheck, effect, ElementRef, inject, NgZone, OnInit, signal, viewChild} from '@angular/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {boundingClientRect} from '@scion/components/dimension';
+
+@Component({
+ selector: 'app-dimension-page',
+ templateUrl: './dimension-page.component.html',
+ styleUrl: './dimension-page.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+})
+export class DimensionPageComponent implements OnInit, DoCheck {
+
+ private _testee = viewChild.required>('testee');
+ private _boundingBox = boundingClientRect(this._testee);
+ private applyButton = viewChild('apply_button', {read: ElementRef});
+
+ protected properties = {
+ x: signal('0px'),
+ y: signal('0px'),
+ width: signal('100px'),
+ height: signal('100px'),
+ };
+
+ protected testeeBoundingBox = {
+ x: signal(undefined),
+ y: signal(undefined),
+ width: signal(undefined),
+ height: signal(undefined),
+ };
+ private zone = inject(NgZone);
+
+ constructor() {
+ effect(() => {
+ const boundingBox = this._boundingBox();
+ this.testeeBoundingBox.x.set(boundingBox.x);
+ this.testeeBoundingBox.y.set(boundingBox.y);
+ this.testeeBoundingBox.width.set(boundingBox.width);
+ this.testeeBoundingBox.height.set(boundingBox.height);
+ });
+ }
+
+ public ngOnInit(): void {
+ this.applyProperties();
+ this.zone.runOutsideAngular(() => {
+ this.applyButton()!.nativeElement.addEventListener('click', () => this.applyProperties());
+ });
+ }
+
+ public ngDoCheck(): void {
+ console.log('[BoundingClientRectPageComponent] Angular change detection cycle');
+ }
+
+ public applyProperties(): void {
+ this._testee().nativeElement.style.left = this.properties.x();
+ this._testee().nativeElement.style.top = this.properties.y();
+ this._testee().nativeElement.style.width = this.properties.width();
+ this._testee().nativeElement.style.height = this.properties.height();
+ }
+}
diff --git a/apps/components-testing-app/src/app/components/dimension/routes.ts b/apps/components-testing-app/src/app/components/dimension/routes.ts
new file mode 100644
index 00000000..379e5f68
--- /dev/null
+++ b/apps/components-testing-app/src/app/components/dimension/routes.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2018-2022 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 {Routes} from '@angular/router';
+import {BoundingClientRectPageComponent} from './bounding-client-rect-page.component';
+
+export default [
+ {path: 'bounding-client-rect', component: BoundingClientRectPageComponent},
+] satisfies Routes;
diff --git a/apps/components-testing-app/src/app/components/routes.ts b/apps/components-testing-app/src/app/components/routes.ts
index 8caf2cb1..f952ea1d 100644
--- a/apps/components-testing-app/src/app/components/routes.ts
+++ b/apps/components-testing-app/src/app/components/routes.ts
@@ -19,4 +19,8 @@ export default [
path: 'sci-sashbox',
loadChildren: () => import('./sci-sashbox/routes'),
},
+ {
+ path: 'dimension',
+ loadChildren: () => import('./dimension/routes'),
+ },
] satisfies Routes;
diff --git a/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.e2e-spec.ts b/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.e2e-spec.ts
new file mode 100644
index 00000000..daa34589
--- /dev/null
+++ b/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.e2e-spec.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 {test} from '../../fixtures';
+import {waitUntilStable} from '../../helper/testing.utils';
+import {expect} from '@playwright/test';
+import {BoundingClientRectPagePO} from './bounding-client-rect-page.po';
+
+test.describe('@scion/components/dimension/boundingClientRect', () => {
+
+ test.only('should not run Angular change detection when changing element size', async ({page, consoleLogs}) => {
+ const testPage = new BoundingClientRectPagePO(page);
+ await testPage.navigate();
+
+ // Set element size to 200x200 pixel.
+ await testPage.enterProperties({x: '0', y: '0', width: '200px', height: '200px'});
+
+ // Clear console logs.
+ await waitUntilStable(() => consoleLogs.get().length);
+ consoleLogs.clear();
+
+ // Apply properties.
+ await testPage.applyProperties();
+ await expect(testPage.testeeBoundingBox.width).toHaveText('200');
+
+ // Expect Angular not to run change detection when changing element size.
+ await waitUntilStable(() => consoleLogs.get().length);
+ await expect.poll(() => consoleLogs.get({message: '[BoundingClientRectPageComponent] Angular change detection cycle'})).toHaveLength(0);
+ });
+});
diff --git a/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.po.ts b/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.po.ts
new file mode 100644
index 00000000..793f3de7
--- /dev/null
+++ b/projects/scion/components.e2e/src/components/dimension/bounding-client-rect-page.po.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018-2022 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 {Locator, Page} from '@playwright/test';
+
+const PATH = '/#/components/dimension/bounding-client-rect';
+
+export class BoundingClientRectPagePO {
+
+ private readonly _locator: Locator;
+
+ public readonly testeeBoundingBox: {
+ x: Locator;
+ y: Locator;
+ width: Locator;
+ height: Locator;
+ };
+
+ constructor(private _page: Page) {
+ this._locator = _page.locator('app-bounding-client-rect-page');
+ this.testeeBoundingBox = {
+ x: this._locator.locator('section.e2e-testee-bounding-box span.e2e-x'),
+ y: this._locator.locator('section.e2e-testee-bounding-box span.e2e-y'),
+ width: this._locator.locator('section.e2e-testee-bounding-box span.e2e-width'),
+ height: this._locator.locator('section.e2e-testee-bounding-box span.e2e-height'),
+ };
+ }
+
+ public async navigate(): Promise {
+ await this._page.goto(PATH);
+ }
+
+ public async enterProperties(properties: {x: string; y: string; width: string; height: string;}): Promise {
+ await this._locator.locator('section.e2e-properties input.e2e-x').fill(properties.x);
+ await this._locator.locator('section.e2e-properties input.e2e-y').fill(properties.y);
+ await this._locator.locator('section.e2e-properties input.e2e-width').fill(properties.width);
+ await this._locator.locator('section.e2e-properties input.e2e-height').fill(properties.height);
+ await this._locator.press('Tab');
+ }
+
+ public async applyProperties(): Promise {
+ await this._locator.locator('section.e2e-properties button.e2e-apply').click();
+ }
+
+ public async resizeWindow(viewportSize: {width?: number; height?: number}): Promise {
+ await this._page.setViewportSize({
+ width: viewportSize.width ?? this._page.viewportSize()!.width,
+ height: viewportSize.height ?? this._page.viewportSize()!.height,
+ });
+ }
+}
diff --git a/projects/scion/components.e2e/src/components/sci-viewport/overlap/viewport-overlap.e2e-spec.ts b/projects/scion/components.e2e/src/components/sci-viewport/overlap/viewport-overlap.e2e-spec.ts
index f6466e01..a5c2a34a 100644
--- a/projects/scion/components.e2e/src/components/sci-viewport/overlap/viewport-overlap.e2e-spec.ts
+++ b/projects/scion/components.e2e/src/components/sci-viewport/overlap/viewport-overlap.e2e-spec.ts
@@ -19,6 +19,6 @@ test.describe('sci-viewport/overlap', () => {
await overlapPO.navigate();
await overlapPO.clickAdjacentElement();
- await expect(await consoleLogs.get({filter: /ViewportOverlapPageComponent/})).toHaveLength(1);
+ await expect(await consoleLogs.get({message: /ViewportOverlapPageComponent/})).toHaveLength(1);
});
});
diff --git a/projects/scion/components.e2e/src/console-logs.ts b/projects/scion/components.e2e/src/console-logs.ts
index 23be4bb7..a04cda66 100644
--- a/projects/scion/components.e2e/src/console-logs.ts
+++ b/projects/scion/components.e2e/src/console-logs.ts
@@ -9,34 +9,31 @@
*/
import {ConsoleMessage, Page} from '@playwright/test';
-import {BehaviorSubject, debounceTime, firstValueFrom} from 'rxjs';
/**
* Collects messages logged to the browser console.
*/
export class ConsoleLogs {
- private _messages$ = new BehaviorSubject([]);
+ private _messages = new Array();
constructor(private _page: Page) {
this._page.on('console', this.onConsole);
}
- public async get(options?: {severity?: Severity; filter?: RegExp; consume?: boolean; probeInterval?: number}): Promise {
- // Wait for log messages to become stable since received asynchronously.
- const messages = await firstValueFrom(this._messages$.pipe(debounceTime(options?.probeInterval ?? 500)));
+ public contains(filter?: {severity?: Severity; message?: RegExp | string}): boolean {
+ return this.get(filter).length > 0;
+ }
- if (options?.consume) {
- this._messages$.next([]);
- }
- return messages
- .filter(message => options?.severity === undefined || message.type() === options.severity)
+ public get(filter?: {severity?: Severity; message?: RegExp | string}): string[] {
+ return this._messages
+ .filter(message => filter?.severity === undefined || message.type() === filter.severity)
.map(message => message.text())
- .filter(message => options?.filter === undefined || message.match(options.filter));
+ .filter(message => filter?.message === undefined || filterMessage(message, filter.message));
}
- public async clear(): Promise {
- await this.get({consume: true});
+ public clear(): void {
+ this._messages.length = 0;
}
public dispose(): void {
@@ -44,10 +41,17 @@ export class ConsoleLogs {
}
private onConsole = (message: ConsoleMessage): void => {
- this._messages$.next([...this._messages$.value, message]);
+ this._messages.push(message);
};
}
+function filterMessage(message: string, filter: string | RegExp): boolean {
+ if (typeof filter === 'string') {
+ return message.includes(filter);
+ }
+ return message.match(filter) !== null;
+}
+
/**
* @see ConsoleMessage#type
*/
diff --git a/projects/scion/components.e2e/src/helper/testing.utils.ts b/projects/scion/components.e2e/src/helper/testing.utils.ts
index 188002ac..d4abfecb 100644
--- a/projects/scion/components.e2e/src/helper/testing.utils.ts
+++ b/projects/scion/components.e2e/src/helper/testing.utils.ts
@@ -9,6 +9,7 @@
*/
import {Locator} from '@playwright/test';
+import {exhaustMap, filter, firstValueFrom, map, pairwise, timer} from 'rxjs';
/**
* Returns `true` if given element is the active element.
@@ -41,6 +42,25 @@ export function fromRect(rect: DOMRectInit | null): DomRect {
};
}
+/**
+ * Waits for a value to become stable.
+ * This function returns the value if it hasn't changed during `probeInterval` (defaults to 100ms).
+ */
+export async function waitUntilStable(value: () => Promise | A, options?: {isStable?: (previous: A, current: A) => boolean; probeInterval?: number}): Promise {
+ if (options?.probeInterval === 0) {
+ return value();
+ }
+
+ const value$ = timer(0, options?.probeInterval ?? 100)
+ .pipe(
+ exhaustMap(async () => await value()),
+ pairwise(),
+ filter(([previous, current]) => options?.isStable ? options.isStable(previous, current) : previous === current),
+ map(([previous]) => previous),
+ );
+ return firstValueFrom(value$);
+}
+
/**
* Position and size of an element.
*/
diff --git a/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.e2e-spec.ts b/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.e2e-spec.ts
index 5ab22ad9..dfe62bd1 100644
--- a/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.e2e-spec.ts
+++ b/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.e2e-spec.ts
@@ -11,6 +11,7 @@
import {test} from '../../../fixtures';
import {expect} from '@playwright/test';
import {BoundingClientRectPagePO} from './bounding-client-rect-page.po';
+import {waitUntilStable} from '../../../helper/testing.utils';
test.describe('@scion/toolkit/observable/fromBoundingClientRect$', () => {
@@ -22,14 +23,36 @@ test.describe('@scion/toolkit/observable/fromBoundingClientRect$', () => {
// Set element size to 100x100 pixel.
await testPage.enterProperties({x: '0', y: '0', width: '100px', height: '100px'});
+ await testPage.applyProperties();
await expect(testPage.testeeBoundingBox.width).toHaveText('100');
// Set element width to 100% and expect the bounding box to be updated.
await testPage.enterProperties({x: '0', y: '0', width: '100%', height: '100px'});
+ await testPage.applyProperties();
await expect(testPage.testeeBoundingBox.width).toHaveText(`${initialWindowWidth}`);
// Increase window width by 200px and expect the bounding box to be updated.
await testPage.resizeWindow({width: initialWindowWidth + 200});
await expect(testPage.testeeBoundingBox.width).toHaveText(`${initialWindowWidth + 200}`);
});
+
+ test('should not run Angular change detection when changing element size', async ({page, consoleLogs}) => {
+ const testPage = new BoundingClientRectPagePO(page);
+ await testPage.navigate();
+
+ // Set element size to 200x200 pixel.
+ await testPage.enterProperties({x: '0', y: '0', width: '200px', height: '200px'});
+
+ // Clear console logs.
+ await waitUntilStable(() => consoleLogs.get().length);
+ consoleLogs.clear();
+
+ // Apply properties.
+ await testPage.applyProperties();
+ await expect(testPage.testeeBoundingBox.width).toHaveText('200');
+
+ // Expect Angular not to run additional change detection when changing element size. (One change detection cycle is triggered by pressing the button)
+ await waitUntilStable(() => consoleLogs.get().length);
+ await expect.poll(() => consoleLogs.get({message: '[BoundingClientRectPageComponent] Angular change detection cycle'})).toHaveLength(1);
+ });
});
diff --git a/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.po.ts b/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.po.ts
index 0f35d467..e159d071 100644
--- a/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.po.ts
+++ b/projects/scion/components.e2e/src/toolkit/observable/bounding-client-rect/bounding-client-rect-page.po.ts
@@ -37,11 +37,15 @@ export class BoundingClientRectPagePO {
await this._page.goto(PATH);
}
- public async enterProperties(properties: {x: string; y: string; width: string; height: string}): Promise {
+ public async enterProperties(properties: {x: string; y: string; width: string; height: string;}): Promise {
await this._locator.locator('section.e2e-properties input.e2e-x').fill(properties.x);
await this._locator.locator('section.e2e-properties input.e2e-y').fill(properties.y);
await this._locator.locator('section.e2e-properties input.e2e-width').fill(properties.width);
await this._locator.locator('section.e2e-properties input.e2e-height').fill(properties.height);
+ await this._locator.press('Tab');
+ }
+
+ public async applyProperties(): Promise {
await this._locator.locator('section.e2e-properties button.e2e-apply').click();
}
diff --git a/projects/scion/components/dimension/src/bounding-client-rect.signal.spec.ts b/projects/scion/components/dimension/src/bounding-client-rect.signal.spec.ts
index 7fb8a9b8..a40cb483 100644
--- a/projects/scion/components/dimension/src/bounding-client-rect.signal.spec.ts
+++ b/projects/scion/components/dimension/src/bounding-client-rect.signal.spec.ts
@@ -9,7 +9,7 @@
*/
import {ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
-import {Component, computed, createEnvironmentInjector, effect, ElementRef, EnvironmentInjector, Injector, runInInjectionContext, signal, Signal, viewChild} from '@angular/core';
+import {Component, computed, createEnvironmentInjector, DoCheck, effect, ElementRef, EnvironmentInjector, Injector, runInInjectionContext, signal, Signal, viewChild} from '@angular/core';
import {firstValueFrom, mergeMap, NEVER, race, throwError, timer} from 'rxjs';
import {toObservable} from '@angular/core/rxjs-interop';
import {first} from 'rxjs/operators';
@@ -527,6 +527,75 @@ describe('Bounding Client Rect Signal', () => {
await expectAsync(waitForSignalChange(boundingBox, {timeout: 500})).toBeRejected();
});
+ xit('should not trigger change detection cycle', async () => {
+ TestBed.configureTestingModule({
+ providers: [
+ {provide: ComponentFixtureAutoDetect, useValue: true},
+ ],
+ teardown: {
+ destroyAfterEach: false,
+ },
+ });
+
+ @Component({
+ selector: 'spec-component',
+ template: `
+
+ `,
+ styles: `
+ div.testee {
+ position: relative;
+ width: 300px;
+ height: 150px;
+ box-sizing: border-box;
+ border: 1px solid red;
+ background-color: blue;
+ }
+ `,
+ })
+ class TestComponent implements DoCheck {
+ public testee = viewChild.required>('testee');
+
+ ngDoCheck(): void {
+ console.log('[TestComponent] Angular change detection cycle');
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const boundingBox = TestBed.runInInjectionContext(() => boundingClientRect(fixture.componentInstance.testee()));
+ const {x, y} = boundingBox();
+
+ // Spy console.
+ console.log('>>> spy console');
+ spyOn(console, 'log').and.callThrough();
+
+ // Expect initial bounding box.
+ expect(boundingBox()).toEqual(jasmine.objectContaining({
+ width: 300,
+ height: 150,
+ x,
+ y,
+ }));
+
+ // Change size.
+ setSize(fixture.componentInstance.testee(), {width: 200, height: 100});
+ await waitForSignalChange(boundingBox);
+ expect(boundingBox()).toEqual(jasmine.objectContaining({
+ width: 200,
+ height: 100,
+ x: x + 10,
+ y: y + 10,
+ }));
+
+ // Expect no change detection cycle to be triggered.
+ expect(console.log).not.toHaveBeenCalledWith('[TestComponent] Angular change detection cycle');
+
+ fixture.detectChanges();
+
+ // Expect no change detection cycle to be triggered.
+ expect(console.log).not.toHaveBeenCalledWith('[TestComponent] Angular change detection cycle');
+ });
+
it('should error if called from within a reactive context', () => {
const boundingBox = computed(() => boundingClientRect(document.body));
expect(() => boundingBox()).toThrowError(/boundingClientRect\(\) cannot be called from within a reactive context/);
diff --git a/projects/scion/components/dimension/src/bounding-client-rect.signal.ts b/projects/scion/components/dimension/src/bounding-client-rect.signal.ts
index c4f0f2c1..954a0352 100644
--- a/projects/scion/components/dimension/src/bounding-client-rect.signal.ts
+++ b/projects/scion/components/dimension/src/bounding-client-rect.signal.ts
@@ -59,7 +59,7 @@ export function boundingClientRect(elementLike: HTMLElement | ElementRef subscription.unsubscribe());
});
- }, {injector});
+ }, {injector, forceRoot: true}); // Create root effect to not trigger change detection cycle on bounding box change.
// Create signal that recomputes each time the bounding box changes.
return computed(() => {
diff --git a/tsconfig.json b/tsconfig.json
index d03b4869..c38b8120 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,17 +22,17 @@
"ES2022",
"dom"
],
- "paths": {
- "@scion/toolkit/*": [
- "./dist/scion/toolkit/*"
- ],
- "@scion/components/*": [
- "./dist/scion/components/*"
- ],
- "@scion/components.internal/*": [
- "./dist/scion/components.internal/*"
- ]
- }
+ // "paths": {
+ // "@scion/toolkit/*": [
+ // "./dist/scion/toolkit/*"
+ // ],
+ // "@scion/components/*": [
+ // "./dist/scion/components/*"
+ // ],
+ // "@scion/components.internal/*": [
+ // "./dist/scion/components.internal/*"
+ // ]
+ // }
/*
* ======================================+
* | PATH-OVERRIDE-FOR-DEVELOPMENT |
@@ -40,80 +40,80 @@
* | DO NOT ACTIVATE FOR PRODUCTION! |
* +=====================================+
*/
- // "paths": {
- // "@scion/components/dimension": [
- // "./projects/scion/components/dimension/src/public_api"
- // ],
- // "@scion/components/sashbox": [
- // "./projects/scion/components/sashbox/src/public_api"
- // ],
- // "@scion/components/splitter": [
- // "./projects/scion/components/splitter/src/public_api"
- // ],
- // "@scion/components/throbber": [
- // "./projects/scion/components/throbber/src/public_api"
- // ],
- // "@scion/components/viewport": [
- // "./projects/scion/components/viewport/src/public_api"
- // ],
- // "@scion/toolkit/bean-manager": [
- // "./projects/scion/toolkit/bean-manager/src/public_api"
- // ],
- // "@scion/toolkit/crypto": [
- // "./projects/scion/toolkit/crypto/src/public_api"
- // ],
- // "@scion/toolkit/observable": [
- // "./projects/scion/toolkit/observable/src/public_api"
- // ],
- // "@scion/toolkit/operators": [
- // "./projects/scion/toolkit/operators/src/public_api"
- // ],
- // "@scion/toolkit/storage": [
- // "./projects/scion/toolkit/storage/src/public_api"
- // ],
- // "@scion/toolkit/testing": [
- // "./projects/scion/toolkit/testing/src/public_api"
- // ],
- // "@scion/toolkit/util": [
- // "./projects/scion/toolkit/util/src/public_api"
- // ],
- // "@scion/toolkit/uuid": [
- // "./projects/scion/toolkit/uuid/src/public_api"
- // ],
- // "@scion/components.internal/accordion": [
- // "./projects/scion/components.internal/accordion/src/public_api"
- // ],
- // "@scion/components.internal/checkbox": [
- // "./projects/scion/components.internal/checkbox/src/public_api"
- // ],
- // "@scion/components.internal/filter-field": [
- // "./projects/scion/components.internal/filter-field/src/public_api"
- // ],
- // "@scion/components.internal/form-field": [
- // "./projects/scion/components.internal/form-field/src/public_api"
- // ],
- // "@scion/components.internal/list": [
- // "./projects/scion/components.internal/list/src/public_api"
- // ],
- // "@scion/components.internal/key-value-field": [
- // "./projects/scion/components.internal/key-value-field/src/public_api"
- // ],
- // "@scion/components.internal/key-value": [
- // "./projects/scion/components.internal/key-value/src/public_api"
- // ],
- // "@scion/components.internal/qualifier-chip-list": [
- // "./projects/scion/components.internal/qualifier-chip-list/src/public_api"
- // ],
- // "@scion/components.internal/tabbar": [
- // "./projects/scion/components.internal/tabbar/src/public_api"
- // ],
- // "@scion/components.internal/toggle-button": [
- // "./projects/scion/components.internal/toggle-button/src/public_api"
- // ],
- // "@scion/components.internal/material-icon": [
- // "./projects/scion/components.internal/material-icon/src/public_api"
- // ],
- // }
+ "paths": {
+ "@scion/components/dimension": [
+ "./projects/scion/components/dimension/src/public_api"
+ ],
+ "@scion/components/sashbox": [
+ "./projects/scion/components/sashbox/src/public_api"
+ ],
+ "@scion/components/splitter": [
+ "./projects/scion/components/splitter/src/public_api"
+ ],
+ "@scion/components/throbber": [
+ "./projects/scion/components/throbber/src/public_api"
+ ],
+ "@scion/components/viewport": [
+ "./projects/scion/components/viewport/src/public_api"
+ ],
+ "@scion/toolkit/bean-manager": [
+ "./projects/scion/toolkit/bean-manager/src/public_api"
+ ],
+ "@scion/toolkit/crypto": [
+ "./projects/scion/toolkit/crypto/src/public_api"
+ ],
+ "@scion/toolkit/observable": [
+ "./projects/scion/toolkit/observable/src/public_api"
+ ],
+ "@scion/toolkit/operators": [
+ "./projects/scion/toolkit/operators/src/public_api"
+ ],
+ "@scion/toolkit/storage": [
+ "./projects/scion/toolkit/storage/src/public_api"
+ ],
+ "@scion/toolkit/testing": [
+ "./projects/scion/toolkit/testing/src/public_api"
+ ],
+ "@scion/toolkit/util": [
+ "./projects/scion/toolkit/util/src/public_api"
+ ],
+ "@scion/toolkit/uuid": [
+ "./projects/scion/toolkit/uuid/src/public_api"
+ ],
+ "@scion/components.internal/accordion": [
+ "./projects/scion/components.internal/accordion/src/public_api"
+ ],
+ "@scion/components.internal/checkbox": [
+ "./projects/scion/components.internal/checkbox/src/public_api"
+ ],
+ "@scion/components.internal/filter-field": [
+ "./projects/scion/components.internal/filter-field/src/public_api"
+ ],
+ "@scion/components.internal/form-field": [
+ "./projects/scion/components.internal/form-field/src/public_api"
+ ],
+ "@scion/components.internal/list": [
+ "./projects/scion/components.internal/list/src/public_api"
+ ],
+ "@scion/components.internal/key-value-field": [
+ "./projects/scion/components.internal/key-value-field/src/public_api"
+ ],
+ "@scion/components.internal/key-value": [
+ "./projects/scion/components.internal/key-value/src/public_api"
+ ],
+ "@scion/components.internal/qualifier-chip-list": [
+ "./projects/scion/components.internal/qualifier-chip-list/src/public_api"
+ ],
+ "@scion/components.internal/tabbar": [
+ "./projects/scion/components.internal/tabbar/src/public_api"
+ ],
+ "@scion/components.internal/toggle-button": [
+ "./projects/scion/components.internal/toggle-button/src/public_api"
+ ],
+ "@scion/components.internal/material-icon": [
+ "./projects/scion/components.internal/material-icon/src/public_api"
+ ]
+ }
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,