Browse Source

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.
pull/24777/head
Fahri Gedik 8 months ago
parent
commit
743d660069
  1. 10
      npm/ng-packs/packages/components/page/src/page.component.html
  2. 34
      npm/ng-packs/packages/components/page/src/page.component.ts
  3. 8
      npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
  4. 15
      npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts
  5. 15
      npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts
  6. 32
      npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts
  7. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html
  8. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts
  9. 71
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  10. 20
      npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts
  11. 20
      npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts
  12. 37
      npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
  13. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
  14. 31
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts

10
npm/ng-packs/packages/components/page/src/page.component.html

@ -3,10 +3,10 @@
@if (customTitle) {
<ng-content select="abp-page-title-container"></ng-content>
} @else {
@if (title) {
@if (title()) {
<div class="col-auto" *abpPagePart="pageParts.title">
<h1 class="content-header-title">
{{ title }}
{{ title() }}
</h1>
</div>
}
@ -25,9 +25,9 @@
@if (customToolbar) {
<ng-content select="abp-page-toolbar-container"></ng-content>
} @else {
@if (toolbarVisible) {
<div class="col" *abpPagePart="pageParts.toolbar; context: toolbarData">
<abp-page-toolbar [record]="toolbarData"></abp-page-toolbar>
@if (toolbarVisible()) {
<div class="col" *abpPagePart="pageParts.toolbar; context: toolbarData()">
<abp-page-toolbar [record]="toolbarData()"></abp-page-toolbar>
</div>
}
}

34
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<string | undefined>(undefined);
readonly toolbarInput = input<any>(undefined, { alias: 'toolbar' });
readonly breadcrumb = input(true);
protected readonly toolbarVisible = signal(false);
protected readonly toolbarData = signal<any>(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 ||

8
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<T = any, U = T> 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<boolean>(undefined);
readonly readonly = input<boolean | undefined>(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);

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

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

32
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<string, string> = {
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<FreeTextType | undefined>(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);
}
}

4
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) {
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a routerLink="/"><i class="fa fa-home" aria-hidden="true"></i> </a>
</li>
@for (item of items; track $index; let last = $last) {
@for (item of items(); track $index; let last = $last) {
<li class="breadcrumb-item" [class.active]="last" aria-current="page">
<ng-container
*ngTemplateOutlet="item.path ? linkTemplate : textTemplate; context: { $implicit: item }"

4
npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
import { ABP, LocalizationPipe } from '@abp/ng.core';
@ -9,5 +9,5 @@ import { ABP, LocalizationPipe } from '@abp/ng.core';
imports: [NgTemplateOutlet, RouterLink, LocalizationPipe],
})
export class BreadcrumbItemsComponent {
@Input() items: Partial<ABP.Route>[] = [];
readonly items = input<Partial<ABP.Route>[]>([]);
}

71
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)"
>
<i [class]="icon" class="me-1" aria-hidden="true"></i><ng-content></ng-content>
<i [class]="icon()" class="me-1" aria-hidden="true"></i><ng-content></ng-content>
</button>
`,
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<string>(undefined);
readonly iconClass = input<string>(undefined);
@Input()
loading = false;
readonly formName = input<string | undefined>(undefined);
readonly iconClass = input<string | undefined>(undefined);
readonly loadingInput = input(false, { alias: 'loading' });
readonly disabled = input<boolean | undefined>(false);
readonly attributes = input<ABP.Dictionary<string> | undefined>(undefined);
readonly attributes = input<ABP.Dictionary<string>>(undefined);
// Internal writable signal for loading state - can be set programmatically
private readonly _loading = signal(false);
@Output() readonly click = new EventEmitter<MouseEvent>();
// Computed that combines input and internal state
readonly isLoading = computed(() => this.loadingInput() || this._loading());
@Output() readonly focus = new EventEmitter<FocusEvent>();
@Output() readonly blur = new EventEmitter<FocusEvent>();
@Output() readonly abpClick = new EventEmitter<MouseEvent>();
@Output() readonly abpFocus = new EventEmitter<FocusEvent>();
// 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<FocusEvent>();
readonly click = output<MouseEvent>();
readonly focus = output<FocusEvent>();
readonly blur = output<FocusEvent>();
readonly abpClick = output<MouseEvent>();
readonly abpFocus = output<FocusEvent>();
readonly abpBlur = output<FocusEvent>();
@ViewChild('button', { static: true })
buttonRef!: ElementRef<HTMLButtonElement>;
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]);
}
});
}

20
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()) {
<label [class]="labelClass()" [for]="checkboxId()">
{{ label | abpLocalization }}
{{ label() | abpLocalization }}
</label>
}
</div>
@ -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<string | undefined>(undefined);
readonly labelClass = input('form-check-label');
readonly checkboxId = input.required<string>();
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<void>();
@Output() checkboxFocus = new EventEmitter<void>();
readonly checkboxBlur = output<void>();
readonly checkboxFocus = output<void>();
}

20
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: `
<div class="mb-3">
@if (label) {
@if (label()) {
<label [class]="labelClass()" [for]="inputId()">
{{ label | abpLocalization }}
{{ label() | abpLocalization }}
</label>
}
<input
@ -18,8 +18,8 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
[readonly]="inputReadonly()"
[class]="inputClass()"
[style]="inputStyle()"
(blur)="formBlur.next()"
(focus)="formFocus.next()"
(blur)="formBlur.emit()"
(focus)="formFocus.emit()"
[(ngModel)]="value"
/>
</div>
@ -36,13 +36,11 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
export class FormInputComponent extends AbstractNgModelComponent {
readonly inputId = input.required<string>();
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<void>();
@Output() formFocus = new EventEmitter<void>();
readonly formBlur = output<void>();
readonly formFocus = output<void>();
}

37
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: `
<div id="abp-loader-bar" [class]="containerClass()" [class.is-loading]="isLoading">
<div id="abp-loader-bar" [class]="containerClass()" [class.is-loading]="isLoading()">
<div
class="abp-progress"
[class.progressing]="progressLevel"
@ -27,31 +27,26 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
private httpWaitService = inject(HttpWaitService);
private routerWaitService = inject(RouterWaitService);
protected _isLoading!: boolean;
@Input()
set isLoading(value: boolean) {
this._isLoading = value;
this.cdRef.detectChanges();
}
get isLoading(): boolean {
return this._isLoading;
}
readonly isLoadingInput = input(false, { alias: 'isLoading' });
readonly containerClass = input('abp-loader-bar');
readonly color = input('#77b6ff');
progressLevel = 0;
protected readonly isLoading = signal(false);
progressLevel = 0;
interval = new Subscription();
timer = new Subscription();
intervalPeriod = 350;
stopDelay = 800;
constructor() {
effect(() => {
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;

2
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html

@ -1,7 +1,7 @@
<div
class="abp-toast-container"
[style.top]="top() || 'auto'"
[style.right]="right || 'auto'"
[style.right]="right() || 'auto'"
[style.bottom]="bottom() || 'auto'"
[style.left]="left() || 'auto'"
[style.display]="toasts.length ? 'flex' : 'none'"

31
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts

@ -1,4 +1,4 @@
import { Component, HostListener, Input, OnInit, input } from '@angular/core';
import { Component, OnInit, input, signal, effect } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { toastInOut } from '../../animations/toast.animations';
import { Toaster } from '../../models/toaster';
@ -10,6 +10,9 @@ import { ToastComponent } from '../toast/toast.component';
styleUrls: ['./toast-container.component.scss'],
animations: [toastInOut],
imports: [ToastComponent],
host: {
'(window:resize)': 'onWindowResize()'
}
})
export class ToastContainerComponent implements OnInit {
toasts$!: ReplaySubject<Toaster.Toast[]>;
@ -18,18 +21,21 @@ export class ToastContainerComponent implements OnInit {
toasts = [] as Toaster.Toast[];
readonly top = input<string>(undefined);
@Input()
right = '30px';
defaultRight = '30px';
defaultMobileRight = '0';
readonly top = input<string | undefined>(undefined);
readonly rightInput = input('30px', { alias: 'right' });
readonly bottom = input('30px');
readonly left = input<string | undefined>(undefined);
readonly toastKey = input<string | undefined>(undefined);
readonly left = input<string>(undefined);
protected readonly right = signal('30px');
readonly defaultRight = '30px';
readonly defaultMobileRight = '0';
readonly toastKey = input<string>(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);
}
}

Loading…
Cancel
Save