diff --git a/api-goldens/element-ng/file-uploader/index.api.md b/api-goldens/element-ng/file-uploader/index.api.md index 06e8850651..0ad513f8bf 100644 --- a/api-goldens/element-ng/file-uploader/index.api.md +++ b/api-goldens/element-ng/file-uploader/index.api.md @@ -10,9 +10,7 @@ import { HttpHeaders } from '@angular/common/http'; import { HttpRequest } from '@angular/common/http'; import { HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { OnChanges } from '@angular/core'; import * as _siemens_element_translate_ng_translate from '@siemens/element-translate-ng/translate'; -import { SimpleChanges } from '@angular/core'; import { Subscription } from 'rxjs'; import { TranslatableString } from '@siemens/element-translate-ng/translate'; @@ -88,7 +86,8 @@ export class SiFileUploadDirective { } // @public (undocumented) -export class SiFileUploaderComponent implements OnChanges { +export class SiFileUploaderComponent { + constructor(); readonly accept: _angular_core.InputSignal; readonly acceptText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; readonly autoUpload: _angular_core.InputSignalWithTransform; diff --git a/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts b/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts index 5e4786c214..49d8fe82e6 100644 --- a/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts +++ b/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; @@ -17,29 +17,30 @@ import { SI_DATATABLE_CONFIG, SiDatatableModule } from '.'; columnMode="force" siDatatableInteraction [cssClasses]="tableConfig.cssClasses" - [rows]="rows" + [rows]="rows()" [columns]="columns" [headerHeight]="tableConfig.headerHeight" [footerHeight]="0" [rowHeight]="tableConfig.rowHeightSmall" [externalPaging]="false" - [selectionType]="selectionType" - [count]="rows.length" - [virtualization]="virtualization" + [selectionType]="selectionType()" + [count]="rows().length" + [virtualization]="virtualization()" [scrollbarV]="true" - [datatableInteractionAutoSelect]="datatableInteractionAutoSelect" + [datatableInteractionAutoSelect]="datatableInteractionAutoSelect()" [(selected)]="selected" /> - ` + `, + changeDetection: ChangeDetectionStrategy.Default }) class WrapperComponent { - selectionType: 'multi' | 'single' | 'cell' = 'multi'; - virtualization = false; - datatableInteractionAutoSelect = false; - selected: any[] = []; + readonly selectionType = signal<'multi' | 'single' | 'cell'>('multi'); + readonly virtualization = signal(false); + readonly datatableInteractionAutoSelect = signal(false); + readonly selected = signal([]); tableConfig = SI_DATATABLE_CONFIG; - rows: any[] = []; + readonly rows = signal([]); columns = [ { prop: 'id', @@ -72,14 +73,16 @@ class WrapperComponent { ]; constructor() { + const rows = []; for (let i = 1; i <= 250; i++) { - this.rows.push({ + rows.push({ id: i, firstname: 'First ' + i, lastname: 'Last ' + i, age: 50 }); } + this.rows.set(rows); } } @@ -158,7 +161,7 @@ describe('SiDatatableInteractionDirective', () => { it.skipIf(!document.hasFocus())( 'should navigate into and inside arrow keys when using virtualization', async () => { - wrapperComponent.virtualization = true; + wrapperComponent.virtualization.set(true); await refresh(); getTableElement().focus(); @@ -205,8 +208,8 @@ describe('SiDatatableInteractionDirective', () => { it.skipIf(!document.hasFocus())( 'should navigate into and inside table using arrow keys when using virtualization and cell selection', async () => { - wrapperComponent.selectionType = 'cell'; - wrapperComponent.virtualization = true; + wrapperComponent.selectionType.set('cell'); + wrapperComponent.virtualization.set(true); await refresh(); getTableElement().focus(); @@ -255,10 +258,10 @@ describe('SiDatatableInteractionDirective', () => { ); it.skipIf(!document.hasFocus())('should auto select on focus when enabled', async () => { - wrapperComponent.selectionType = 'single'; - wrapperComponent.datatableInteractionAutoSelect = true; + wrapperComponent.selectionType.set('single'); + wrapperComponent.datatableInteractionAutoSelect.set(true); await refresh(); - expect(wrapperComponent.selected).toHaveLength(0); + expect(wrapperComponent.selected()).toHaveLength(0); const row = getTableElement().querySelector( '.datatable-row-wrapper > .datatable-body-row' @@ -267,7 +270,7 @@ describe('SiDatatableInteractionDirective', () => { await refresh(); - expect(wrapperComponent.selected).toContain({ + expect(wrapperComponent.selected()).toContain({ id: 1, firstname: 'First 1', lastname: 'Last 1', @@ -278,10 +281,10 @@ describe('SiDatatableInteractionDirective', () => { it.skipIf(!document.hasFocus())( 'should not auto select on mouse click when enabled', async () => { - wrapperComponent.selectionType = 'single'; - wrapperComponent.datatableInteractionAutoSelect = true; + wrapperComponent.selectionType.set('single'); + wrapperComponent.datatableInteractionAutoSelect.set(true); await refresh(); - expect(wrapperComponent.selected).toHaveLength(0); + expect(wrapperComponent.selected()).toHaveLength(0); const table = getTableElement(); @@ -296,7 +299,7 @@ describe('SiDatatableInteractionDirective', () => { await refresh(); - expect(wrapperComponent.selected).toHaveLength(0); + expect(wrapperComponent.selected()).toHaveLength(0); table.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); @@ -306,7 +309,7 @@ describe('SiDatatableInteractionDirective', () => { await refresh(); - expect(wrapperComponent.selected).toContain({ + expect(wrapperComponent.selected()).toContain({ id: 1, firstname: 'First 1', lastname: 'Last 1', diff --git a/projects/element-ng/electron-titlebar/si-electron-titlebar.component.ts b/projects/element-ng/electron-titlebar/si-electron-titlebar.component.ts index c2eeb54b82..312ea5751c 100644 --- a/projects/element-ng/electron-titlebar/si-electron-titlebar.component.ts +++ b/projects/element-ng/electron-titlebar/si-electron-titlebar.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ import { CdkMenuTrigger } from '@angular/cdk/menu'; -import { booleanAttribute, Component, input, output } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { elementLeft4, elementOptionsVertical, elementRight4 } from '@siemens/element-icons'; import { MenuItem as MenuItemLegacy } from '@siemens/element-ng/common'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; @@ -14,7 +14,8 @@ import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; selector: 'si-electron-titlebar', imports: [CdkMenuTrigger, SiMenuFactoryComponent, SiIconComponent, SiTranslatePipe], templateUrl: './si-electron-titlebar.component.html', - styleUrl: './si-electron-titlebar.component.scss' + styleUrl: './si-electron-titlebar.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiElectrontitlebarComponent { /** diff --git a/projects/element-ng/file-uploader/si-file-uploader.component.html b/projects/element-ng/file-uploader/si-file-uploader.component.html index c1e54870d2..9a2fe9399f 100644 --- a/projects/element-ng/file-uploader/si-file-uploader.component.html +++ b/projects/element-ng/file-uploader/si-file-uploader.component.html @@ -14,7 +14,7 @@ (filesAdded)="handleFiles($event)" /> -@if (maxFilesReached) { +@if (maxFilesReached()) { - @for (file of files; track file) { + @for (file of files(); track file) {
@@ -63,7 +63,7 @@ diff --git a/projects/element-ng/file-uploader/si-file-uploader.component.ts b/projects/element-ng/file-uploader/si-file-uploader.component.ts index fff4903661..6f1e2097da 100644 --- a/projects/element-ng/file-uploader/si-file-uploader.component.ts +++ b/projects/element-ng/file-uploader/si-file-uploader.component.ts @@ -13,14 +13,15 @@ import { } from '@angular/common/http'; import { booleanAttribute, - ChangeDetectorRef, + ChangeDetectionStrategy, Component, + effect, inject, input, numberAttribute, - OnChanges, output, - SimpleChanges, + signal, + untracked, viewChild } from '@angular/core'; import { elementCancel, elementDelete, elementDocument, elementRedo } from '@siemens/element-icons'; @@ -89,9 +90,10 @@ interface ExtUploadFile extends UploadFile { SiTranslatePipe ], templateUrl: './si-file-uploader.component.html', - styleUrl: './si-file-uploader.component.scss' + styleUrl: './si-file-uploader.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) -export class SiFileUploaderComponent implements OnChanges { +export class SiFileUploaderComponent { /** * Text of the link to open the file select dialog (follows `uploadDropText`). * @@ -342,20 +344,22 @@ export class SiFileUploaderComponent implements OnChanges { elementDocument, elementRedo }); - protected files: ExtUploadFile[] = []; + protected readonly files = signal([]); protected pending = 0; private uploading = 0; - protected uploadEnabled = false; - protected maxFilesReached = false; + protected readonly uploadEnabled = signal(false); + protected readonly maxFilesReached = signal(false); private readonly dropZone = viewChild.required('dropZone'); - private cdRef = inject(ChangeDetectorRef); private http? = inject(HttpClient, { optional: true }); - ngOnChanges(changes: SimpleChanges): void { - if (changes.maxFiles || changes.disableUpload) { - this.updateStates(); - } + constructor() { + effect(() => { + // Track only these inputs; use untracked to avoid tracking files() inside updateStates() + this.maxFiles(); + this.disableUpload(); + untracked(() => this.updateStates()); + }); } protected handleFiles(files: UploadFile[]): void { @@ -365,13 +369,14 @@ export class SiFileUploaderComponent implements OnChanges { const maxFiles = this.maxFiles(); // for single-file case, replace exiting file if any - if (maxFiles === 1 && this.files.length) { + if (maxFiles === 1 && this.files().length) { this.reset(false); } let numValid = this.countValid(); + const current = this.files().slice(); for (const file of files) { - const duplicate = this.isDuplicate(file); + const duplicate = this.isDuplicate(file, current); if (duplicate) { // in case this is duplicated: reset if already uploaded or not handled yet if (duplicate.status !== 'uploading' && duplicate.status !== 'queued') { @@ -383,34 +388,31 @@ export class SiFileUploaderComponent implements OnChanges { const canAdd = numValid + 1 <= maxFiles; const valid = file.status === 'added'; if (valid && !canAdd) { - this.maxFilesReached = true; + this.maxFilesReached.set(true); break; } else if (valid) { numValid++; } - this.files.push(file); + current.push(file); } - this.files.sort((a, b) => a.fileName.localeCompare(b.fileName)); - - this.filesChanges.emit(this.files.slice()); + current.sort((a, b) => a.fileName.localeCompare(b.fileName)); + this.files.set(current); + this.filesChanges.emit(current.slice()); this.updateStates(); - // needed for drag drop of directory - this.cdRef.markForCheck(); - if (this.autoUpload()) { // allow concurrent uploads for auto upload mode. - this.uploadEnabled = true; + this.uploadEnabled.set(true); this.fileUpload(false); } } protected removeFile(index: number): void { if (index >= 0) { - this.files.splice(index, 1); - this.filesChanges.emit(this.files.slice()); + this.files.update(current => current.filter((_, i) => i !== index)); + this.filesChanges.emit(this.files()); this.dropZone().reset(); this.updateStates(); } @@ -425,6 +427,7 @@ export class SiFileUploaderComponent implements OnChanges { this.pending--; file.status = 'added'; file.progress = 0; + this.files.update(f => [...f]); this.updateStates(); const { status, fileName, size, progress } = file; @@ -446,8 +449,8 @@ export class SiFileUploaderComponent implements OnChanges { * Reset the state. */ reset(emit = true): void { - this.files.forEach(f => f.subscription?.unsubscribe()); - this.files = []; + this.files().forEach(f => f.subscription?.unsubscribe()); + this.files.set([]); this.dropZone().reset(); this.updateStates(); if (emit) { @@ -459,11 +462,11 @@ export class SiFileUploaderComponent implements OnChanges { * Uploads the file */ fileUpload(doRetry = true): void { - if (!this.uploadEnabled) { + if (!this.uploadEnabled()) { return; } - this.uploadEnabled = false; - this.doUpload(this.files, doRetry); + this.uploadEnabled.set(false); + this.doUpload(this.files(), doRetry); } private doUpload(files: UploadFile[], doRetry: boolean): void { @@ -478,8 +481,8 @@ export class SiFileUploaderComponent implements OnChanges { } private processQueue(): void { - for (let i = 0; i < this.files.length && this.uploading < this.maxConcurrentUploads(); i++) { - const file = this.files[i]; + for (let i = 0; i < this.files().length && this.uploading < this.maxConcurrentUploads(); i++) { + const file = this.files()[i]; if (file.status === 'queued') { this.uploading++; this.uploadOneFile(file); @@ -513,6 +516,7 @@ export class SiFileUploaderComponent implements OnChanges { file.status = 'uploading'; file.errorText = undefined; file.httpErrorText = undefined; + this.files.update(f => [...f]); const requestHandler = config.handler ?? @@ -531,8 +535,8 @@ export class SiFileUploaderComponent implements OnChanges { } // this is a light check for duplicate file - name and size only, not content! - private isDuplicate(file: UploadFile): UploadFile | null { - for (const uploadFile of this.files) { + private isDuplicate(file: UploadFile, current: ExtUploadFile[]): UploadFile | null { + for (const uploadFile of current) { if (uploadFile.file.name === file.file.name && uploadFile.file.size === file.file.size) { return uploadFile; } @@ -545,7 +549,7 @@ export class SiFileUploaderComponent implements OnChanges { file.successResponse = httpEvent as HttpResponse; } else if (httpEvent.type === HttpEventType.UploadProgress && httpEvent.total) { file.progress = Math.floor((100 * httpEvent.loaded) / httpEvent.total); - this.cdRef.markForCheck(); + this.files.update(f => [...f]); } } @@ -574,6 +578,7 @@ export class SiFileUploaderComponent implements OnChanges { file.subscription = undefined; this.pending--; this.uploading--; + this.files.update(f => [...f]); this.updateStates(); this.processQueue(); } @@ -581,26 +586,25 @@ export class SiFileUploaderComponent implements OnChanges { private fadeOut(file: ExtUploadFile): void { setTimeout(() => { file.fadeOut = true; - this.cdRef.markForCheck(); + this.files.update(f => [...f]); setTimeout(() => { - this.removeFile(this.files.indexOf(file)); - this.cdRef.markForCheck(); + this.removeFile(this.files().indexOf(file)); }, 500); - this.cdRef.markForCheck(); }, 3500); } private updateStates(): void { - this.uploadEnabled = + this.uploadEnabled.set( !this.disableUpload() && - !this.pending && - this.files.some(f => f.status === 'added' || f.status === 'error'); - if (this.maxFilesReached && this.countValid() < this.maxFiles()) { - this.maxFilesReached = false; + !this.pending && + this.files().some(f => f.status === 'added' || f.status === 'error') + ); + if (this.maxFilesReached() && this.countValid() < this.maxFiles()) { + this.maxFilesReached.set(false); } } private countValid(): number { - return this.files.reduce((acc, f) => acc + (f.status !== 'invalid' ? 1 : 0), 0); + return this.files().reduce((acc, f) => acc + (f.status !== 'invalid' ? 1 : 0), 0); } } diff --git a/projects/element-ng/filter-bar/si-filter-bar.component.ts b/projects/element-ng/filter-bar/si-filter-bar.component.ts index d795db41ba..39eb28fdbd 100644 --- a/projects/element-ng/filter-bar/si-filter-bar.component.ts +++ b/projects/element-ng/filter-bar/si-filter-bar.component.ts @@ -2,7 +2,15 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { booleanAttribute, Component, input, model, output, viewChild } from '@angular/core'; +import { + booleanAttribute, + ChangeDetectionStrategy, + Component, + input, + model, + output, + viewChild +} from '@angular/core'; import { SiAutoCollapsableListAdditionalContentDirective, SiAutoCollapsableListDirective, @@ -27,6 +35,7 @@ import { SiFilterPillComponent } from './si-filter-pill.component'; ], templateUrl: './si-filter-bar.component.html', styleUrl: './si-filter-bar.component.scss', + changeDetection: ChangeDetectionStrategy.Default, host: { '[class.reset]': 'allowReset()' } diff --git a/projects/element-ng/filter-bar/si-filter-pill.component.ts b/projects/element-ng/filter-bar/si-filter-pill.component.ts index 271d90bf5e..be4d6b9e14 100644 --- a/projects/element-ng/filter-bar/si-filter-pill.component.ts +++ b/projects/element-ng/filter-bar/si-filter-pill.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ import { NgTemplateOutlet } from '@angular/common'; -import { booleanAttribute, Component, input, output } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { elementCancel } from '@siemens/element-icons'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; import { SiTranslatePipe } from '@siemens/element-translate-ng/translate'; @@ -14,7 +14,8 @@ import { Filter } from './filter'; selector: 'si-filter-pill', imports: [NgTemplateOutlet, SiIconComponent, SiTranslatePipe], templateUrl: './si-filter-pill.component.html', - styleUrl: './si-filter-pill.component.scss' + styleUrl: './si-filter-pill.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiFilterPillComponent { protected readonly icons = addIcons({ elementCancel }); diff --git a/projects/element-ng/filtered-search/si-filtered-search.component.ts b/projects/element-ng/filtered-search/si-filtered-search.component.ts index b0272c03e9..dad321976f 100644 --- a/projects/element-ng/filtered-search/si-filtered-search.component.ts +++ b/projects/element-ng/filtered-search/si-filtered-search.component.ts @@ -4,6 +4,7 @@ */ import { booleanAttribute, + ChangeDetectionStrategy, Component, computed, DestroyRef, @@ -59,6 +60,7 @@ import { ], templateUrl: './si-filtered-search.component.html', styleUrl: './si-filtered-search.component.scss', + changeDetection: ChangeDetectionStrategy.Default, host: { '[class.disabled]': 'disabled()', '[class.dark-background]': "colorVariant() === 'base-0'" diff --git a/projects/element-ng/footer/si-footer.component.ts b/projects/element-ng/footer/si-footer.component.ts index 8c5eea4aac..751da30728 100644 --- a/projects/element-ng/footer/si-footer.component.ts +++ b/projects/element-ng/footer/si-footer.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { Link, SiLinkDirective } from '@siemens/element-ng/link'; import { SiTranslatePipe } from '@siemens/element-translate-ng/translate'; @@ -10,7 +10,8 @@ import { SiTranslatePipe } from '@siemens/element-translate-ng/translate'; selector: 'si-footer', imports: [SiLinkDirective, SiTranslatePipe], templateUrl: './si-footer.component.html', - styleUrl: './si-footer.component.scss' + styleUrl: './si-footer.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiFooterComponent { /** diff --git a/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts b/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts index 64d812e783..b80622b24c 100644 --- a/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts +++ b/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts @@ -53,7 +53,8 @@ export class TestHostComponent {
- ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostWithNestingComponent { form = new FormGroup({}); @@ -68,7 +69,6 @@ describe('SiFormContainerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - ReactiveFormsModule, SiFormModule.withConfiguration({ validationErrorMapper: { minlength: 'custom-length-message' } }), diff --git a/projects/element-ng/form/si-form-container/si-form-container.component.ts b/projects/element-ng/form/si-form-container/si-form-container.component.ts index 6f843c2fea..113154bedf 100644 --- a/projects/element-ng/form/si-form-container/si-form-container.component.ts +++ b/projects/element-ng/form/si-form-container/si-form-container.component.ts @@ -3,7 +3,14 @@ * SPDX-License-Identifier: MIT */ import { NgTemplateOutlet } from '@angular/common'; -import { booleanAttribute, Component, computed, inject, input } from '@angular/core'; +import { + booleanAttribute, + ChangeDetectionStrategy, + Component, + computed, + inject, + input +} from '@angular/core'; import { AbstractControl, FormGroup } from '@angular/forms'; import { Breakpoints, SiResponsiveContainerDirective } from '@siemens/element-ng/resize-observer'; import { TranslatableString } from '@siemens/element-translate-ng/translate'; @@ -23,6 +30,7 @@ export interface SiFormValidationError { imports: [NgTemplateOutlet, SiResponsiveContainerDirective], templateUrl: './si-form-container.component.html', styleUrl: './si-form-container.component.scss', + changeDetection: ChangeDetectionStrategy.Default, host: { '[style.--si-form-label-width]': 'labelWidth()' } diff --git a/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.component.ts b/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.component.ts index d8a6aa320d..b27524de3a 100644 --- a/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.component.ts +++ b/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, inject, InjectionToken, Signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, InjectionToken, Signal } from '@angular/core'; import { SiTranslatePipe } from '@siemens/element-translate-ng/translate'; import { SiFormError } from '../si-form-item/si-form-item.component'; @@ -19,6 +19,7 @@ export const SI_FORM_VALIDATION_TOOLTIP_DATA = new InjectionToken{{ error.message | translate: error.params }}
} `, + changeDetection: ChangeDetectionStrategy.Default, host: { 'class': 'd-flex flex-column gap-2 text-start' } diff --git a/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.spec.ts b/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.spec.ts index d93ae55b99..0ba3325708 100644 --- a/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.spec.ts +++ b/projects/element-ng/form/si-form-validation-tooltip/si-form-validation-tooltip.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @@ -13,7 +13,8 @@ import { SiFormValidationTooltipDirective } from './si-form-validation-tooltip.d describe('SiFormValidationTooltipDirective', () => { @Component({ imports: [SiFormValidationTooltipDirective, ReactiveFormsModule], - template: `` + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { control = new FormControl(''); diff --git a/projects/element-ng/form/si-form.spec.ts b/projects/element-ng/form/si-form.spec.ts index f4897c871f..bfd387e35d 100644 --- a/projects/element-ng/form/si-form.spec.ts +++ b/projects/element-ng/form/si-form.spec.ts @@ -4,7 +4,7 @@ */ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, @@ -157,7 +157,8 @@ describe('SiForm', () => { - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { form = new FormGroup({ @@ -190,7 +191,7 @@ describe('SiForm', () => { it('should update required indicator', async () => { fixture.componentInstance.form.controls.input.setValidators([]); - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.form.controls.input.updateValueAndValidity(); await fixture.whenStable(); const field = await loader.getHarness(SiFormItemHarness.with({ label: 'Input' })); expect(await field.isRequired()).toBe(false); @@ -203,14 +204,15 @@ describe('SiForm', () => { template: `
- +
- ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { - value = ''; - required = true; + readonly value = signal(''); + readonly required = signal(true); } let fixture: ComponentFixture; @@ -228,8 +230,8 @@ describe('SiForm', () => { it('should update required indicator', async () => { const field = await loader.getHarness(SiFormItemHarness.with({ label: 'Input' })); expect(await field.isRequired()).toBe(true); - fixture.componentInstance.required = false; - fixture.changeDetectorRef.markForCheck(); + fixture.componentInstance.required.set(false); + await fixture.whenStable(); expect(await field.isRequired()).toBe(false); }); @@ -282,7 +284,8 @@ describe('SiForm', () => { - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { normal = new FormControl(true); diff --git a/projects/element-ng/header-dropdown/si-header-dropdown-items-factory.component.ts b/projects/element-ng/header-dropdown/si-header-dropdown-items-factory.component.ts index a84cf2afca..ed12394b32 100644 --- a/projects/element-ng/header-dropdown/si-header-dropdown-items-factory.component.ts +++ b/projects/element-ng/header-dropdown/si-header-dropdown-items-factory.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { MenuItem } from '@siemens/element-ng/common'; import { SiLinkDirective } from '@siemens/element-ng/link'; import { SiTranslatePipe } from '@siemens/element-translate-ng/translate'; @@ -26,7 +26,8 @@ import { SiHeaderDropdownComponent } from './si-header-dropdown.component'; SiLinkDirective, SiHeaderDropdownTriggerDirective ], - templateUrl: './si-header-dropdown-items-factory.component.html' + templateUrl: './si-header-dropdown-items-factory.component.html', + changeDetection: ChangeDetectionStrategy.Default }) export class SiHeaderDropdownItemsFactoryComponent { readonly items = input.required(); diff --git a/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts b/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts index bebc1d810f..427f8f8f05 100644 --- a/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts +++ b/projects/element-ng/header-dropdown/si-header-dropdown-trigger.directive.ts @@ -5,6 +5,7 @@ import { Overlay, OverlayRef } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; import { + ChangeDetectionStrategy, Component, ComponentRef, Directive, @@ -25,7 +26,11 @@ import { filter, skip, take, takeUntil } from 'rxjs/operators'; import { SI_HEADER_WITH_DROPDOWNS } from './si-header.model'; -@Component({ template: '', host: { '[attr.aria-owns]': 'ariaOwns()' } }) +@Component({ + template: '', + changeDetection: ChangeDetectionStrategy.Default, + host: { '[attr.aria-owns]': 'ariaOwns()' } +}) class SiHeaderAnchorComponent { readonly ariaOwns = input(); } diff --git a/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts b/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts index 6f58cb6c9a..aa8850aadb 100644 --- a/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts +++ b/projects/element-ng/header-dropdown/si-header-dropdown.directive.spec.ts @@ -4,7 +4,7 @@ */ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BehaviorSubject } from 'rxjs'; @@ -40,7 +40,8 @@ import { SiHeaderDropdownTriggerHarness } from './testing/si-header-dropdown-tri `, - providers: [{ provide: SI_HEADER_WITH_DROPDOWNS, useExisting: TestHostComponent }] + providers: [{ provide: SI_HEADER_WITH_DROPDOWNS, useExisting: TestHostComponent }], + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent implements HeaderWithDropdowns { inlineDropdown = new BehaviorSubject(false); diff --git a/projects/element-ng/help-button/si-help-button.component.ts b/projects/element-ng/help-button/si-help-button.component.ts index cd086dc29d..22d0a4aa8c 100644 --- a/projects/element-ng/help-button/si-help-button.component.ts +++ b/projects/element-ng/help-button/si-help-button.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { elementHelp } from '@siemens/element-icons'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; import { SiPopoverDirective } from '@siemens/element-ng/popover'; @@ -32,6 +32,7 @@ import { SiPopoverDirective } from '@siemens/element-ng/popover'; `, styleUrl: './si-help-button.component.scss', + changeDetection: ChangeDetectionStrategy.Default, hostDirectives: [ { directive: SiPopoverDirective, diff --git a/projects/element-ng/icon/si-icon.component.spec.ts b/projects/element-ng/icon/si-icon.component.spec.ts index 612d3033df..631074acd4 100644 --- a/projects/element-ng/icon/si-icon.component.spec.ts +++ b/projects/element-ng/icon/si-icon.component.spec.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { SiThemeService } from '@siemens/element-ng/theme'; @@ -26,7 +26,8 @@ describe('SiSvgIconComponent', () => { @Component({ imports: [SiIconComponent], - template: ` ` + template: ` `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { readonly icon = signal('element-user'); @@ -132,7 +133,8 @@ describe('SiSvgIconComponent', () => { @Component({ selector: 'si-icon-1-test', imports: [SiIconComponent], - template: `` + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush }) class IconTest1Component { readonly icons = addIcons({ @@ -143,7 +145,8 @@ describe('SiSvgIconComponent', () => { @Component({ selector: 'si-icon-2-test', imports: [SiIconComponent], - template: `` + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush }) class IconTest2Component { icons = addIcons({ @@ -159,7 +162,8 @@ describe('SiSvgIconComponent', () => { } @if (show2()) { - }` + }`, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { readonly show1 = signal(false); diff --git a/projects/element-ng/inline-notification/si-inline-notification.component.ts b/projects/element-ng/inline-notification/si-inline-notification.component.ts index e20dfc41a4..51fb740019 100644 --- a/projects/element-ng/inline-notification/si-inline-notification.component.ts +++ b/projects/element-ng/inline-notification/si-inline-notification.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { booleanAttribute, Component, input } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, input } from '@angular/core'; import { StatusType } from '@siemens/element-ng/common'; import { SiStatusIconComponent } from '@siemens/element-ng/icon'; import { Link, SiLinkDirective } from '@siemens/element-ng/link'; @@ -12,7 +12,8 @@ import { SiTranslatePipe, TranslatableString } from '@siemens/element-translate- selector: 'si-inline-notification', imports: [SiLinkDirective, SiTranslatePipe, SiStatusIconComponent], templateUrl: './si-inline-notification.component.html', - styleUrl: './si-inline-notification.component.scss' + styleUrl: './si-inline-notification.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiInlineNotificationComponent { /** diff --git a/projects/element-ng/language-switcher/si-language-switcher.component.ts b/projects/element-ng/language-switcher/si-language-switcher.component.ts index 7a8814eeaa..448f26c271 100644 --- a/projects/element-ng/language-switcher/si-language-switcher.component.ts +++ b/projects/element-ng/language-switcher/si-language-switcher.component.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, computed, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { injectSiTranslateService, SiTranslatePipe, @@ -15,7 +15,8 @@ import { IsoLanguageValue } from './iso-language-value'; selector: 'si-language-switcher', imports: [SiTranslatePipe], templateUrl: './si-language-switcher.component.html', - styleUrl: './si-language-switcher.component.scss' + styleUrl: './si-language-switcher.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiLanguageSwitcherComponent { /** diff --git a/projects/element-ng/link/si-link.directive.spec.ts b/projects/element-ng/link/si-link.directive.spec.ts index f7dac004a6..79f9b14d34 100644 --- a/projects/element-ng/link/si-link.directive.spec.ts +++ b/projects/element-ng/link/si-link.directive.spec.ts @@ -35,7 +35,8 @@ class TestHostComponent { @Component({ selector: 'si-empty', - template: `empty` + template: `empty`, + changeDetection: ChangeDetectionStrategy.OnPush }) class SiEmptyComponent {} diff --git a/projects/element-ng/list-details/si-list-details.component.spec.ts b/projects/element-ng/list-details/si-list-details.component.spec.ts index d8b146b0bb..ea8d820b3c 100644 --- a/projects/element-ng/list-details/si-list-details.component.spec.ts +++ b/projects/element-ng/list-details/si-list-details.component.spec.ts @@ -371,11 +371,16 @@ describe('ListDetailsComponent', () => { template: ` Header Body - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class DetailsComponent {} - @Component({ selector: 'si-empty', template: 'EMPTY' }) + @Component({ + selector: 'si-empty', + template: 'EMPTY', + changeDetection: ChangeDetectionStrategy.OnPush + }) class EmptyComponent {} @Component({ @@ -387,7 +392,8 @@ describe('ListDetailsComponent', () => { - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class ListComponent {} diff --git a/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts b/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts index cc9d9a7188..a35ce8ce2b 100644 --- a/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts +++ b/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SiLoadingSpinnerModule } from './si-loading-spinner.module'; @@ -19,7 +19,8 @@ import { SiLoadingSpinnerModule } from './si-loading-spinner.module'; et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
- ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) export class TestHostComponent { readonly loading = signal(true); diff --git a/projects/element-ng/markdown-renderer/si-markdown-renderer.component.ts b/projects/element-ng/markdown-renderer/si-markdown-renderer.component.ts index eb03e3bc59..cb44241087 100644 --- a/projects/element-ng/markdown-renderer/si-markdown-renderer.component.ts +++ b/projects/element-ng/markdown-renderer/si-markdown-renderer.component.ts @@ -2,7 +2,14 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, effect, inject, input, ElementRef } from '@angular/core'; +import { + Component, + effect, + inject, + input, + ElementRef, + ChangeDetectionStrategy +} from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { getMarkdownRenderer } from './markdown-renderer'; @@ -13,7 +20,8 @@ import { getMarkdownRenderer } from './markdown-renderer'; */ @Component({ selector: 'si-markdown-renderer', - template: `` + template: ``, + changeDetection: ChangeDetectionStrategy.Default }) export class SiMarkdownRendererComponent { private sanitizer = inject(DomSanitizer); diff --git a/projects/element-ng/menu/si-menu-factory.component.ts b/projects/element-ng/menu/si-menu-factory.component.ts index 59b37fb0ab..8d72db3096 100644 --- a/projects/element-ng/menu/si-menu-factory.component.ts +++ b/projects/element-ng/menu/si-menu-factory.component.ts @@ -4,7 +4,7 @@ */ import { CdkMenuGroup, CdkMenuTrigger } from '@angular/cdk/menu'; import { NgTemplateOutlet } from '@angular/common'; -import { Component, inject, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; import { RouterLink } from '@angular/router'; import { MenuItem as MenuItemLegacy } from '@siemens/element-ng/common'; import { SiLinkActionService, SiLinkModule } from '@siemens/element-ng/link'; @@ -37,7 +37,8 @@ import { SiMenuDirective } from './si-menu.directive'; RouterLink, SiMenuFactoryItemGuardDirective ], - templateUrl: './si-menu-factory.component.html' + templateUrl: './si-menu-factory.component.html', + changeDetection: ChangeDetectionStrategy.Default }) export class SiMenuFactoryComponent { readonly items = input(); diff --git a/projects/element-ng/menu/si-menu.spec.ts b/projects/element-ng/menu/si-menu.spec.ts index 75d6bcefa8..062c78fca2 100644 --- a/projects/element-ng/menu/si-menu.spec.ts +++ b/projects/element-ng/menu/si-menu.spec.ts @@ -5,7 +5,7 @@ import { CdkMenuTrigger } from '@angular/cdk/menu'; import { ComponentHarness, HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MenuItem } from '@siemens/element-ng/common'; import { SiLinkActionService } from '@siemens/element-ng/link'; @@ -27,7 +27,8 @@ class ButtonHarness extends ComponentHarness { - ` + `, + changeDetection: ChangeDetectionStrategy.Default }) class TestLegacyObjectComponent { items: MenuItem[] = [ diff --git a/projects/element-ng/modal/si-modal-service.spec.ts b/projects/element-ng/modal/si-modal-service.spec.ts index 2cfe0cce5c..5965cfb4f5 100644 --- a/projects/element-ng/modal/si-modal-service.spec.ts +++ b/projects/element-ng/modal/si-modal-service.spec.ts @@ -2,13 +2,21 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { ApplicationRef, Component, input, TemplateRef, viewChild } from '@angular/core'; +import { + ApplicationRef, + ChangeDetectionStrategy, + Component, + input, + TemplateRef, + viewChild +} from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { SiModalService } from './si-modal.service'; @Component({ - template: '
test component-{{inputProp()}}
' + template: '
test component-{{inputProp()}}
', + changeDetection: ChangeDetectionStrategy.OnPush }) class DialogComponent { readonly inputProp = input(); @@ -19,7 +27,8 @@ class DialogComponent {
test template
- ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class DialogTemplateComponent { readonly templateRef = viewChild.required>('ref'); diff --git a/projects/element-ng/notification-item/si-notification-item.component.ts b/projects/element-ng/notification-item/si-notification-item.component.ts index 72c1d6ef4b..342cd872c0 100644 --- a/projects/element-ng/notification-item/si-notification-item.component.ts +++ b/projects/element-ng/notification-item/si-notification-item.component.ts @@ -4,7 +4,7 @@ */ import { CdkMenuTrigger } from '@angular/cdk/menu'; import { CommonModule } from '@angular/common'; -import { booleanAttribute, Component, inject, input } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; import { ActivatedRoute, RouterModule, type NavigationExtras } from '@angular/router'; import { elementOptionsVertical } from '@siemens/element-icons'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; @@ -152,7 +152,8 @@ export type NotificationItemPrimaryAction = SiIconComponent ], templateUrl: './si-notification-item.component.html', - styleUrl: './si-notification-item.component.scss' + styleUrl: './si-notification-item.component.scss', + changeDetection: ChangeDetectionStrategy.Default }) export class SiNotificationItemComponent { protected readonly icons = addIcons({ elementOptionsVertical }); diff --git a/projects/element-ng/number-input/si-number-input.component.spec.ts b/projects/element-ng/number-input/si-number-input.component.spec.ts index da848b0bcb..96a49a4d2d 100644 --- a/projects/element-ng/number-input/si-number-input.component.spec.ts +++ b/projects/element-ng/number-input/si-number-input.component.spec.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: MIT */ import { + ChangeDetectionStrategy, Component, inject, inputBinding, @@ -26,7 +27,8 @@ import { SiNumberInputComponent } from './si-number-input.component'; [min]="min()" [max]="max()" /> - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class FormHostComponent { readonly required = signal(false); @@ -38,7 +40,8 @@ class FormHostComponent { @Component({ imports: [SiNumberInputComponent], - template: ` ` + template: ` `, + changeDetection: ChangeDetectionStrategy.OnPush }) class AttributeComponent { readonly siNumberInput = viewChild.required(SiNumberInputComponent); diff --git a/projects/element-ng/password-toggle/si-password-toggle.component.spec.ts b/projects/element-ng/password-toggle/si-password-toggle.component.spec.ts index 6df504e5fe..50a5c485e8 100644 --- a/projects/element-ng/password-toggle/si-password-toggle.component.spec.ts +++ b/projects/element-ng/password-toggle/si-password-toggle.component.spec.ts @@ -2,7 +2,7 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, @@ -27,7 +27,8 @@ const rgbToHex = (rgb: string): string => { - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class TestHostComponent { readonly showVisibilityIcon = input(true); @@ -41,7 +42,8 @@ class TestHostComponent { - ` + `, + changeDetection: ChangeDetectionStrategy.OnPush }) class FormHostComponent { readonly form = new FormGroup({ diff --git a/projects/element-ng/password-toggle/si-password-toggle.component.ts b/projects/element-ng/password-toggle/si-password-toggle.component.ts index 24198af6b6..e0af3d2c71 100644 --- a/projects/element-ng/password-toggle/si-password-toggle.component.ts +++ b/projects/element-ng/password-toggle/si-password-toggle.component.ts @@ -2,7 +2,14 @@ * Copyright (c) Siemens 2016 - 2026 * SPDX-License-Identifier: MIT */ -import { Component, contentChild, input, output, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + contentChild, + input, + output, + signal +} from '@angular/core'; import { NgControl } from '@angular/forms'; import { elementHide, elementShow } from '@siemens/element-icons'; import { addIcons, SiIconComponent } from '@siemens/element-ng/icon'; @@ -13,6 +20,7 @@ import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; imports: [SiIconComponent, SiTranslatePipe], templateUrl: './si-password-toggle.component.html', styleUrl: './si-password-toggle.component.scss', + changeDetection: ChangeDetectionStrategy.Default, host: { class: 'form-control-wrapper', '[class.show-visibility-icon]': 'showVisibilityIcon()'