From 0c09657851bd96c454489a40bf190c552b75bc66 Mon Sep 17 00:00:00 2001 From: Fahri Gedik <53567152+fahrigedik@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:40:09 +0300 Subject: [PATCH] Refactor components to use Angular signal inputs/outputs Migrates Tree, FeatureManagement, and PermissionManagement components to use Angular's new signal-based input and output APIs. Updates templates and internal logic to access input values as functions, replaces EventEmitter with output signals, and synchronizes internal state using signals and effects. Also updates related model interfaces to use InputSignal and OutputEmitterRef types. --- .../src/lib/components/tree.component.html | 8 +- .../tree/src/lib/components/tree.component.ts | 121 ++++++++++------ .../feature-management.component.html | 6 +- .../feature-management.component.ts | 62 +++++---- .../src/lib/models/feature-management.ts | 9 +- .../permission-management.component.html | 6 +- .../permission-management.component.ts | 130 ++++++++++-------- .../src/lib/models/permission-management.ts | 11 +- 8 files changed, 213 insertions(+), 140 deletions(-) diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html index 8cd2058699..4980e78273 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html @@ -1,8 +1,8 @@ diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts index 5c320a539b..f8c624ec9d 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts @@ -3,13 +3,14 @@ import { ChangeDetectorRef, Component, ContentChild, - EventEmitter, inject, - Input, OnInit, - Output, TemplateRef, ViewEncapsulation, + input, + output, + signal, + effect } from '@angular/core'; import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'; import { @@ -56,32 +57,66 @@ export class TreeComponent implements OnInit { private cdr = inject(ChangeDetectorRef); private disableTreeStyleLoading = inject(DISABLE_TREE_STYLE_LOADING_TOKEN, { optional: true }); - dropPosition: number; + dropPosition!: number; dropdowns = {} as { [key: string]: NgbDropdown }; - @ContentChild('menu') menu: TemplateRef; - @ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective; - @ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective; - @Output() readonly checkedKeysChange = new EventEmitter(); - @Output() readonly expandedKeysChange = new EventEmitter(); - @Output() readonly selectedNodeChange = new EventEmitter(); - @Output() readonly dropOver = new EventEmitter(); - @Output() readonly nzExpandChange = new EventEmitter(); - @Input() noAnimation = true; - @Input() draggable: boolean; - @Input() checkable: boolean; - @Input() checkStrictly: boolean; - @Input() checkedKeys = []; - @Input() nodes = []; - @Input() expandedKeys: string[] = []; - @Input() selectedNode: any; - @Input() changeCheckboxWithNode: boolean; - @Input() isNodeSelected = node => this.selectedNode?.id === node.key; - @Input() beforeDrop = (event: NzFormatBeforeDropEvent) => { - this.dropPosition = event.pos; - return of(false); - }; + @ContentChild('menu') menu!: TemplateRef; + @ContentChild(TreeNodeTemplateDirective) customNodeTemplate!: TreeNodeTemplateDirective; + @ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate!: ExpandedIconTemplateDirective; + + // Output signals + readonly checkedKeysChange = output(); + readonly expandedKeysChange = output(); + readonly selectedNodeChange = output(); + readonly dropOver = output(); + readonly nzExpandChange = output(); + + // Input signals + readonly noAnimation = input(true); + readonly draggable = input(undefined); + readonly checkable = input(undefined); + readonly checkStrictly = input(undefined); + readonly checkedKeysInput = input([], { alias: 'checkedKeys' }); + readonly nodesInput = input([], { alias: 'nodes' }); + readonly expandedKeysInput = input([], { alias: 'expandedKeys' }); + readonly selectedNodeInput = input(undefined, { alias: 'selectedNode' }); + readonly changeCheckboxWithNode = input(undefined); + readonly isNodeSelectedFn = input<(node: any) => boolean>( + (node) => this._selectedNode() === node.key, + { alias: 'isNodeSelected' } + ); + readonly beforeDropFn = input<(event: NzFormatBeforeDropEvent) => any>( + (event: NzFormatBeforeDropEvent) => { + this.dropPosition = event.pos; + return of(false); + }, + { alias: 'beforeDrop' } + ); + + // Internal signals for two-way binding + protected readonly _checkedKeys = signal([]); + protected readonly _expandedKeys = signal([]); + protected readonly _selectedNode = signal(undefined); + protected readonly _nodes = signal([]); + + // Getters for template access + get checkedKeys() { return this._checkedKeys(); } + get expandedKeys() { return this._expandedKeys(); } + get selectedNode() { return this._selectedNode(); } + get nodes() { return this._nodes(); } + get isNodeSelected() { return this.isNodeSelectedFn(); } + get beforeDrop() { return this.beforeDropFn(); } + + constructor() { + // Sync input signals to internal signals + effect(() => { + this._checkedKeys.set(this.checkedKeysInput()); + this._expandedKeys.set(this.expandedKeysInput()); + this._selectedNode.set(this.selectedNodeInput()); + this._nodes.set(this.nodesInput()); + }); + } ngOnInit() { this.loadStyle(); @@ -97,13 +132,13 @@ export class TreeComponent implements OnInit { this.subscriptionService.addOne(loaded$); } - private findNode(target: any, nodes: any[]) { + private findNode(target: any, nodes: any[]): any { for (const node of nodes) { if (node.key === target.id) { return node; } if (node.children) { - const res = this.findNode(target, node.children); + const res: any = this.findNode(target, node.children); if (res) { return res; } @@ -113,36 +148,36 @@ export class TreeComponent implements OnInit { } onSelectedNodeChange(node: NzTreeNode) { - this.selectedNode = node.origin.entity; - if (this.changeCheckboxWithNode) { + this._selectedNode.set(node.origin.entity); + if (this.changeCheckboxWithNode()) { let newVal; if (node.isChecked) { - newVal = this.checkedKeys.filter(x => x !== node.key); + newVal = this._checkedKeys().filter(x => x !== node.key); } else { - newVal = [...this.checkedKeys, node.key]; + newVal = [...this._checkedKeys(), node.key]; } this.selectedNodeChange.emit(node); - this.checkedKeys = newVal; + this._checkedKeys.set(newVal); this.checkedKeysChange.emit(newVal); } else { this.selectedNodeChange.emit(node.origin.entity); } } - onCheckboxChange(event) { - this.checkedKeys = [...event.keys]; + onCheckboxChange(event: { keys: any[] }) { + this._checkedKeys.set([...event.keys]); this.checkedKeysChange.emit(event.keys); } - onExpandedKeysChange(event) { - this.expandedKeys = [...event.keys]; + onExpandedKeysChange(event: { keys: string[] } & NzFormatEmitEvent) { + this._expandedKeys.set([...event.keys]); this.expandedKeysChange.emit(event.keys); this.nzExpandChange.emit(event); } onDrop(event: DropEvent) { - event.event.stopPropagation(); - event.event.preventDefault(); + event.event?.stopPropagation(); + event.event?.preventDefault(); event.pos = this.dropPosition; this.dropOver.emit(event); @@ -160,12 +195,14 @@ export class TreeComponent implements OnInit { dropdown.close(); } }); - this.dropdowns[dropdownKey]?.toggle(); + if (dropdownKey) { + this.dropdowns[dropdownKey]?.toggle(); + } } setSelectedNode(node: any) { - const newSelectedNode = this.findNode(node, this.nodes); - this.selectedNode = { ...newSelectedNode }; + const newSelectedNode = this.findNode(node, this._nodes()); + this._selectedNode.set({ ...newSelectedNode }); this.cdr.markForCheck(); } } diff --git a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html index f59a66b6a5..bd08cbb7b7 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html @@ -3,8 +3,8 @@

{{ 'AbpFeatureManagement::Features' | abpLocalization }} - @if (providerTitle) { - - {{ providerTitle }} + @if (providerTitle()) { + - {{ providerTitle() }} }

@@ -38,7 +38,7 @@ @for (feature of features[group.name]; track feature.id || i; let i = $index) { @let provider = feature.provider.name; @let isFeatureDisabled = !feature.parentName ? isParentDisabled(feature.name, group.name, provider) : - (provider !== providerName && provider !== defaultProviderName); + (provider !== providerName() && provider !== defaultProviderName);
@switch (feature.valueType?.name) { diff --git a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts index 5feac210e5..47bfc3e96e 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, inject, DOCUMENT } from '@angular/core'; +import { Component, inject, DOCUMENT, input, output, signal, effect } from '@angular/core'; import { NgTemplateOutlet } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ConfigStateService, LocalizationPipe, TrackByService } from '@abp/ng.core'; @@ -61,14 +61,17 @@ export class FeatureManagementComponent protected readonly confirmationService = inject(ConfirmationService); private document = inject(DOCUMENT); - @Input() - providerKey: string; + // Signal inputs + readonly providerKey = input(undefined); + readonly providerName = input(undefined); + readonly providerTitle = input(undefined); + readonly visibleInput = input(false, { alias: 'visible' }); - @Input() - providerName: string; + // Output signals + readonly visibleChange = output(); - @Input({ required: false }) - providerTitle: string; + // Internal state + protected readonly _visible = signal(false); selectedGroupDisplayName: string; @@ -82,33 +85,41 @@ export class FeatureManagementComponent defaultProviderName = DEFAULT_PROVIDER_NAME; - protected _visible; + modalBusy = false; - @Input() + // Getter/setter for backward compatibility get visible(): boolean { - return this._visible; + return this._visible(); } set visible(value: boolean) { - if (this._visible === value) { + if (this._visible() === value) { return; } - this._visible = value; + this._visible.set(value); this.visibleChange.emit(value); if (value) { this.openModal(); - return; } } - @Output() readonly visibleChange = new EventEmitter(); - - modalBusy = false; + constructor() { + // Sync visible input to internal signal + effect(() => { + const inputValue = this.visibleInput(); + if (this._visible() !== inputValue) { + this._visible.set(inputValue); + if (inputValue) { + this.openModal(); + } + } + }); + } openModal() { - if (!this.providerName) { + if (!this.providerName()) { throw new Error('providerName is required.'); } @@ -116,7 +127,7 @@ export class FeatureManagementComponent } getFeatures() { - this.service.get(this.providerName, this.providerKey).subscribe(res => { + this.service.get(this.providerName()!, this.providerKey()).subscribe(res => { if (!res.groups?.length) return; this.groups = res.groups.map(({ name, displayName }) => ({ name, displayName })); this.selectedGroupDisplayName = this.groups[0].displayName; @@ -149,13 +160,13 @@ export class FeatureManagementComponent this.modalBusy = true; this.service - .update(this.providerName, this.providerKey, { features: changedFeatures }) + .update(this.providerName()!, this.providerKey(), { features: changedFeatures }) .pipe(finalize(() => (this.modalBusy = false))) .subscribe(() => { this.visible = false; this.toasterService.success('AbpUi::SavedSuccessfully'); - if (!this.providerKey) { + if (!this.providerKey()) { // to refresh host's features this.configState.refreshAppState().subscribe(); } @@ -167,11 +178,11 @@ export class FeatureManagementComponent .warn('AbpFeatureManagement::AreYouSureToResetToDefault', 'AbpFeatureManagement::AreYouSure') .subscribe((status: Confirmation.Status) => { if (status === Confirmation.Status.confirm) { - this.service.delete(this.providerName, this.providerKey).subscribe(() => { + this.service.delete(this.providerName()!, this.providerKey()).subscribe(() => { this.toasterService.success('AbpFeatureManagement::ResetedToDefault'); this.visible = false; - if (!this.providerKey) { + if (!this.providerKey()) { // to refresh host's features this.configState.refreshAppState().subscribe(); } @@ -190,17 +201,18 @@ export class FeatureManagementComponent isParentDisabled(parentName: string, groupName: string, provider: string): boolean { const children = this.features[groupName]?.filter(f => f.parentName === parentName); + const providerNameValue = this.providerName(); if (children?.length) { return children.some(child => { const childProvider = child.provider?.name; return ( - (childProvider !== this.providerName && childProvider !== this.defaultProviderName) || - (provider !== this.providerName && provider !== this.defaultProviderName) + (childProvider !== providerNameValue && childProvider !== this.defaultProviderName) || + (provider !== providerNameValue && provider !== this.defaultProviderName) ); }); } else { - return provider !== this.providerName && provider !== this.defaultProviderName; + return provider !== providerNameValue && provider !== this.defaultProviderName; } } diff --git a/npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts b/npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts index c04d917515..31a7efd22c 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/models/feature-management.ts @@ -1,13 +1,14 @@ -import { EventEmitter } from '@angular/core'; +import { InputSignal, OutputEmitterRef } from '@angular/core'; export namespace FeatureManagement { export interface FeatureManagementComponentInputs { visible: boolean; - readonly providerName: string; - readonly providerKey: string; + readonly providerName: InputSignal; + readonly providerKey: InputSignal; } export interface FeatureManagementComponentOutputs { - readonly visibleChange: EventEmitter; + readonly visibleChange: OutputEmitterRef; } } + diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html index 0224f3d45a..0711bb995b 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html @@ -1,9 +1,9 @@ - @if (data.entityDisplayName || entityDisplayName) { + @if (data.entityDisplayName || entityDisplayName()) {

{{ 'AbpPermissionManagement::Permissions' | abpLocalization }} - - {{ entityDisplayName || data.entityDisplayName }} + {{ entityDisplayName() || data.entityDisplayName }}

@@ -115,7 +115,7 @@ />