From 743d660069171e7a24e553c92932a1600e2b1b27 Mon Sep 17 00:00:00 2001
From: Fahri Gedik <53567152+fahrigedik@users.noreply.github.com>
Date: Sat, 31 Jan 2026 12:56:12 +0300
Subject: [PATCH] Refactor components to use Angular signals and input() API
Migrates multiple components and directives to use Angular's new input() and signal() APIs, replacing legacy @Input and @Output decorators. Updates templates to use signal-based accessors, improves reactivity with effect(), and modernizes event emitters with output(). This refactor enhances type safety, consistency, and future-proofs the codebase for upcoming Angular versions.
---
.../components/page/src/page.component.html | 10 +--
.../components/page/src/page.component.ts | 34 ++++-----
.../src/lib/abstracts/ng-model.component.ts | 8 +--
.../src/lib/directives/autofocus.directive.ts | 15 +---
.../lib/directives/show-password.directive.ts | 15 ++--
.../directives/free-text-input.directive.ts | 32 +++++----
.../breadcrumb-items.component.html | 4 +-
.../breadcrumb-items.component.ts | 4 +-
.../lib/components/button/button.component.ts | 71 ++++++++++---------
.../components/checkbox/checkbox.component.ts | 20 +++---
.../form-input/form-input.component.ts | 20 +++---
.../loader-bar/loader-bar.component.ts | 37 +++++-----
.../toast-container.component.html | 2 +-
.../toast-container.component.ts | 31 ++++----
14 files changed, 150 insertions(+), 153 deletions(-)
diff --git a/npm/ng-packs/packages/components/page/src/page.component.html b/npm/ng-packs/packages/components/page/src/page.component.html
index 8cbaf2fdda..45e22c5e37 100644
--- a/npm/ng-packs/packages/components/page/src/page.component.html
+++ b/npm/ng-packs/packages/components/page/src/page.component.html
@@ -3,10 +3,10 @@
@if (customTitle) {
} @else {
- @if (title) {
+ @if (title()) {
}
@@ -25,9 +25,9 @@
@if (customToolbar) {
} @else {
- @if (toolbarVisible) {
-
-
+ @if (toolbarVisible()) {
+
}
}
diff --git a/npm/ng-packs/packages/components/page/src/page.component.ts b/npm/ng-packs/packages/components/page/src/page.component.ts
index 3ee5b7c2db..4c9e35a2b7 100644
--- a/npm/ng-packs/packages/components/page/src/page.component.ts
+++ b/npm/ng-packs/packages/components/page/src/page.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, ViewEncapsulation, ContentChild, input } from '@angular/core';
+import { Component, ViewEncapsulation, ContentChild, input, effect, signal } from '@angular/core';
import {
PageTitleContainerComponent,
PageBreadcrumbContainerComponent,
@@ -16,21 +16,13 @@ import { PagePartDirective } from './page-part.directive';
imports: [BreadcrumbComponent, PageToolbarComponent, PagePartDirective],
})
export class PageComponent {
- @Input() title?: string;
-
- toolbarVisible = false;
- _toolbarData: any;
- @Input() set toolbar(val: any) {
- this._toolbarData = val;
- this.toolbarVisible = true;
- }
-
- get toolbarData() {
- return this._toolbarData;
- }
-
+ readonly title = input
(undefined);
+ readonly toolbarInput = input(undefined, { alias: 'toolbar' });
readonly breadcrumb = input(true);
+ protected readonly toolbarVisible = signal(false);
+ protected readonly toolbarData = signal(undefined);
+
pageParts = {
title: PageParts.title,
breadcrumb: PageParts.breadcrumb,
@@ -42,10 +34,20 @@ export class PageComponent {
customBreadcrumb?: PageBreadcrumbContainerComponent;
@ContentChild(PageToolbarContainerComponent) customToolbar?: PageToolbarContainerComponent;
+ constructor() {
+ effect(() => {
+ const toolbar = this.toolbarInput();
+ if (toolbar !== undefined) {
+ this.toolbarData.set(toolbar);
+ this.toolbarVisible.set(true);
+ }
+ });
+ }
+
get shouldRenderRow() {
return !!(
- this.title ||
- this.toolbarVisible ||
+ this.title() ||
+ this.toolbarVisible() ||
this.breadcrumb() ||
this.customTitle ||
this.customBreadcrumb ||
diff --git a/npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts b/npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
index 6f313d513b..889143c9ed 100644
--- a/npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
+++ b/npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectorRef, Component, inject, Input, input } from '@angular/core';
+import { ChangeDetectorRef, Component, inject, input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
// Not an abstract class on purpose. Do not change!
@@ -11,16 +11,16 @@ export class AbstractNgModelComponent implements ControlValueAcc
onChange?: (value: T) => void;
onTouched?: () => void;
- @Input()
+ // Note: disabled needs to remain as a regular property because setDisabledState assigns to it
disabled?: boolean;
- readonly readonly = input(undefined);
+ readonly readonly = input(undefined);
readonly valueFn = input<(value: U, previousValue?: T) => T>(value => value as any as T);
readonly valueLimitFn = input<(value: T, previousValue?: T) => any>(value => false);
- @Input()
+ // Note: value needs getter/setter for ControlValueAccessor and two-way binding
set value(value: T) {
value = this.valueFn()(value as any as U, this._value);
diff --git a/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts
index 398eaef9cc..65be9c348a 100644
--- a/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts
+++ b/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Directive, ElementRef, Input, inject } from '@angular/core';
+import { AfterViewInit, Directive, ElementRef, inject, input, numberAttribute } from '@angular/core';
@Directive({
selector: '[autofocus]',
@@ -6,18 +6,9 @@ import { AfterViewInit, Directive, ElementRef, Input, inject } from '@angular/co
export class AutofocusDirective implements AfterViewInit {
private elRef = inject(ElementRef);
- private _delay = 0;
-
- @Input('autofocus')
- set delay(val: number | string | undefined) {
- this._delay = Number(val) || 0;
- }
-
- get delay() {
- return this._delay;
- }
+ readonly delay = input(0, { alias: 'autofocus', transform: numberAttribute });
ngAfterViewInit(): void {
- setTimeout(() => this.elRef.nativeElement.focus(), this.delay as number);
+ setTimeout(() => this.elRef.nativeElement.focus(), this.delay());
}
}
diff --git a/npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts
index a8d8addd6c..367f483b21 100644
--- a/npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts
+++ b/npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, ElementRef, Input, inject } from '@angular/core';
+import { Directive, ElementRef, effect, inject, input } from '@angular/core';
@Directive({
selector: '[abpShowPassword]',
@@ -6,10 +6,15 @@ import { Directive, ElementRef, Input, inject } from '@angular/core';
export class ShowPasswordDirective {
protected readonly elementRef = inject(ElementRef);
- @Input() set abpShowPassword(visible: boolean) {
- const element = this.elementRef.nativeElement as HTMLInputElement;
- if (!element) return;
+ readonly abpShowPassword = input(false);
- element.type = visible ? 'text' : 'password';
+ constructor() {
+ effect(() => {
+ const visible = this.abpShowPassword();
+ const element = this.elementRef.nativeElement as HTMLInputElement;
+ if (!element) return;
+
+ element.type = visible ? 'text' : 'password';
+ });
}
}
diff --git a/npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts b/npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts
index 6b6556dcfa..259d2f76a1 100644
--- a/npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts
+++ b/npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, HostBinding, Input } from '@angular/core';
+import { Directive, effect, inject, input, signal, ElementRef, Renderer2 } from '@angular/core';
// TODO: improve this type
export interface FreeTextType {
@@ -9,7 +9,7 @@ export interface FreeTextType {
};
}
-export const INPUT_TYPES = {
+export const INPUT_TYPES: Record = {
numeric: 'number',
default: 'text',
};
@@ -19,21 +19,23 @@ export const INPUT_TYPES = {
exportAs: 'inputAbpFeatureManagementFreeText',
})
export class FreeTextInputDirective {
- _feature: FreeTextType;
- // eslint-disable-next-line @angular-eslint/no-input-rename
- @Input('abpFeatureManagementFreeText') set feature(val: FreeTextType) {
- this._feature = val;
- this.setInputType();
- }
+ private readonly elRef = inject(ElementRef);
+ private readonly renderer = inject(Renderer2);
- get feature() {
- return this._feature;
- }
+ readonly feature = input(undefined, { alias: 'abpFeatureManagementFreeText' });
- @HostBinding('type') type: string;
+ constructor() {
+ effect(() => {
+ const feature = this.feature();
+ if (feature) {
+ this.setInputType(feature);
+ }
+ });
+ }
- private setInputType() {
- const validatorType = this.feature?.valueType?.validator?.name.toLowerCase();
- this.type = INPUT_TYPES[validatorType] ?? INPUT_TYPES.default;
+ private setInputType(feature: FreeTextType) {
+ const validatorType = feature?.valueType?.validator?.name?.toLowerCase();
+ const type = INPUT_TYPES[validatorType] ?? INPUT_TYPES['default'];
+ this.renderer.setAttribute(this.elRef.nativeElement, 'type', type);
}
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html
index 4b2dbf611d..4480e34c96 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html
@@ -1,9 +1,9 @@
-@if (items.length) {
+@if (items().length) {
-
- @for (item of items; track $index; let last = $last) {
+ @for (item of items(); track $index; let last = $last) {
-
[] = [];
+ readonly items = input[]>([]);
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
index 652b484085..52da336999 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
@@ -2,14 +2,15 @@
import {
Component,
ElementRef,
- EventEmitter,
- Input,
OnInit,
- Output,
Renderer2,
ViewChild,
+ effect,
inject,
- input
+ input,
+ output,
+ signal,
+ computed
} from '@angular/core';
import { ABP, StopPropagationDirective } from '@abp/ng.core';
@@ -22,12 +23,12 @@ import { ABP, StopPropagationDirective } from '@abp/ng.core';
[attr.type]="buttonType()"
[attr.form]="formName()"
[class]="buttonClass()"
- [disabled]="loading || disabled()"
- (click.stop)="click.next($event); abpClick.next($event)"
- (focus)="focus.next($event); abpFocus.next($event)"
- (blur)="blur.next($event); abpBlur.next($event)"
+ [disabled]="isLoading() || disabled()"
+ (click.stop)="click.emit($event); abpClick.emit($event)"
+ (focus)="focus.emit($event); abpFocus.emit($event)"
+ (blur)="blur.emit($event); abpBlur.emit($event)"
>
-
+
`,
imports: [StopPropagationDirective],
@@ -36,48 +37,48 @@ export class ButtonComponent implements OnInit {
private renderer = inject(Renderer2);
readonly buttonId = input('');
-
readonly buttonClass = input('btn btn-primary');
-
readonly buttonType = input('button');
-
- readonly formName = input(undefined);
-
- readonly iconClass = input(undefined);
-
- @Input()
- loading = false;
-
+ readonly formName = input(undefined);
+ readonly iconClass = input(undefined);
+ readonly loadingInput = input(false, { alias: 'loading' });
readonly disabled = input(false);
+ readonly attributes = input | undefined>(undefined);
- readonly attributes = input>(undefined);
+ // Internal writable signal for loading state - can be set programmatically
+ private readonly _loading = signal(false);
- @Output() readonly click = new EventEmitter();
+ // Computed that combines input and internal state
+ readonly isLoading = computed(() => this.loadingInput() || this._loading());
- @Output() readonly focus = new EventEmitter();
-
- @Output() readonly blur = new EventEmitter();
-
- @Output() readonly abpClick = new EventEmitter();
-
- @Output() readonly abpFocus = new EventEmitter();
+ // Getter/setter for backward compatibility (used by ModalComponent)
+ get loading(): boolean {
+ return this._loading();
+ }
+ set loading(value: boolean) {
+ this._loading.set(value);
+ }
- @Output() readonly abpBlur = new EventEmitter();
+ readonly click = output();
+ readonly focus = output();
+ readonly blur = output();
+ readonly abpClick = output();
+ readonly abpFocus = output();
+ readonly abpBlur = output();
@ViewChild('button', { static: true })
buttonRef!: ElementRef;
- get icon(): string {
- return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass() || 'd-none'}`;
- }
+ protected readonly icon = computed(() => {
+ return this.isLoading() ? 'fa fa-spinner fa-spin' : this.iconClass() || 'd-none';
+ });
ngOnInit() {
const attributes = this.attributes();
if (attributes) {
Object.keys(attributes).forEach(key => {
- const attributesValue = this.attributes();
- if (attributesValue?.[key]) {
- this.renderer.setAttribute(this.buttonRef.nativeElement, key, attributesValue[key]);
+ if (attributes[key]) {
+ this.renderer.setAttribute(this.buttonRef.nativeElement, key, attributes[key]);
}
});
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts
index 850aaf5d01..22636f0af9 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, forwardRef, Input, Output, input } from '@angular/core';
+import { Component, forwardRef, input, output } from '@angular/core';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
@@ -13,12 +13,12 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
[readonly]="checkboxReadonly()"
[class]="checkboxClass()"
[style]="checkboxStyle()"
- (blur)="checkboxBlur.next()"
- (focus)="checkboxFocus.next()"
+ (blur)="checkboxBlur.emit()"
+ (focus)="checkboxFocus.emit()"
/>
- @if (label) {
+ @if (label()) {
}
@@ -33,14 +33,12 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
imports: [FormsModule, LocalizationPipe],
})
export class FormCheckboxComponent extends AbstractNgModelComponent {
- @Input() label?: string;
+ readonly label = input(undefined);
readonly labelClass = input('form-check-label');
readonly checkboxId = input.required();
- readonly checkboxStyle = input<{
- [klass: string]: any;
-} | null | undefined>(undefined);
+ readonly checkboxStyle = input<{ [klass: string]: any } | null | undefined>(undefined);
readonly checkboxClass = input('form-check-input');
readonly checkboxReadonly = input(false);
- @Output() checkboxBlur = new EventEmitter();
- @Output() checkboxFocus = new EventEmitter();
+ readonly checkboxBlur = output();
+ readonly checkboxFocus = output();
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts
index 46bc10fa53..9ea55fb144 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, forwardRef, Input, Output, input } from '@angular/core';
+import { Component, forwardRef, input, output } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
@@ -6,9 +6,9 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
selector: 'abp-form-input',
template: `
- @if (label) {
+ @if (label()) {
}
@@ -36,13 +36,11 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
export class FormInputComponent extends AbstractNgModelComponent {
readonly inputId = input.required();
readonly inputReadonly = input(false);
- @Input() label = '';
+ readonly label = input('');
readonly labelClass = input('form-label');
readonly inputPlaceholder = input('');
- readonly inputStyle = input<{
- [klass: string]: any;
-} | null | undefined>(undefined);
+ readonly inputStyle = input<{ [klass: string]: any } | null | undefined>(undefined);
readonly inputClass = input('form-control');
- @Output() formBlur = new EventEmitter();
- @Output() formFocus = new EventEmitter();
+ readonly formBlur = output();
+ readonly formFocus = output();
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
index d4ed4de9dd..efe34b2e78 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
@@ -1,11 +1,11 @@
-import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, inject, input } from '@angular/core';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject, input, effect, signal } from '@angular/core';
import { combineLatest, Subscription, timer } from 'rxjs';
import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng.core';
@Component({
selector: 'abp-loader-bar',
template: `
-
+
{
+ const value = this.isLoadingInput();
+ this.isLoading.set(value);
+ this.cdRef.detectChanges();
+ });
+ }
+
private readonly clearProgress = () => {
this.progressLevel = 0;
this.cdRef.detectChanges();
@@ -93,9 +88,9 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
}
startLoading() {
- if (this.isLoading || !this.interval.closed) return;
+ if (this.isLoading() || !this.interval.closed) return;
- this.isLoading = true;
+ this.isLoading.set(true);
this.progressLevel = 0;
this.cdRef.detectChanges();
this.interval = timer(0, this.intervalPeriod).subscribe(this.reportProgress);
@@ -106,7 +101,7 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
this.interval.unsubscribe();
this.progressLevel = 100;
- this.isLoading = false;
+ this.isLoading.set(false);
if (!this.timer.closed) return;
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
index 0a70984b37..9969fd693f 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
@@ -1,7 +1,7 @@
;
@@ -18,18 +21,21 @@ export class ToastContainerComponent implements OnInit {
toasts = [] as Toaster.Toast[];
- readonly top = input(undefined);
-
- @Input()
- right = '30px';
- defaultRight = '30px';
- defaultMobileRight = '0';
-
+ readonly top = input(undefined);
+ readonly rightInput = input('30px', { alias: 'right' });
readonly bottom = input('30px');
+ readonly left = input(undefined);
+ readonly toastKey = input(undefined);
- readonly left = input(undefined);
+ protected readonly right = signal('30px');
+ readonly defaultRight = '30px';
+ readonly defaultMobileRight = '0';
- readonly toastKey = input(undefined);
+ constructor() {
+ effect(() => {
+ this.right.set(this.rightInput());
+ });
+ }
ngOnInit() {
this.setDefaultRight();
@@ -42,15 +48,14 @@ export class ToastContainerComponent implements OnInit {
});
}
- @HostListener('window:resize')
onWindowResize() {
this.setDefaultRight();
}
setDefaultRight() {
const screenWidth = window.innerWidth;
- if (screenWidth < 768 && this.right == this.defaultRight) {
- this.right = this.defaultMobileRight;
+ if (screenWidth < 768 && this.right() === this.defaultRight) {
+ this.right.set(this.defaultMobileRight);
}
}