Browse Source

Refactor to use Angular signals and input() API

Refactors extensible form, table, and related components to use Angular's signal-based input() API instead of @Input and ngOnChanges. Updates internal state management to use signals and effects, improves type safety with ReadonlyPropData, and updates templates to use new signal-based accessors. Removes legacy OnChanges logic and adapts utility and directive code to the new reactive paradigm.
pull/24777/head
Fahri Gedik 8 months ago
parent
commit
cb1116a8e7
  1. 54
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html
  2. 108
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  3. 4
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html
  4. 32
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts
  5. 18
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  6. 180
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  7. 5
      npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts
  8. 41
      npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts
  9. 5
      npm/ng-packs/packages/components/extensible/src/lib/models/actions.ts
  10. 27
      npm/ng-packs/packages/components/extensible/src/lib/models/props.ts
  11. 10
      npm/ng-packs/packages/components/extensible/src/lib/utils/form-props.util.ts
  12. 45
      npm/ng-packs/packages/components/page/src/page-part.directive.ts

54
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html

@ -1,34 +1,34 @@
<ng-container *abpPermission="prop.permission; runChangeDetection: false"> <ng-container *abpPermission="prop().permission; runChangeDetection: false">
@switch (getComponent(prop)) { @switch (getComponent(prop())) {
@case ('template') { @case ('template') {
<ng-container *ngComponentOutlet="prop.template; injector: injectorForCustomComponent" /> <ng-container *ngComponentOutlet="prop().template; injector: injectorForCustomComponent" />
} }
} }
<div [class]="containerClassName" class="mb-2"> <div [class]="containerClassName" class="mb-2">
@switch (getComponent(prop)) { @switch (getComponent(prop())) {
@case ('input') { @case ('input') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<input #field [id]="prop.id" [formControlName]="prop.name" [autocomplete]="prop.autocomplete" [type]="getType(prop)" <input #field [id]="prop().id" [formControlName]="prop().name" [autocomplete]="prop().autocomplete" [type]="getType(prop())"
[abpDisabled]="disabled" [readonly]="readonly" class="form-control" /> [abpDisabled]="disabled" [readonly]="readonly" class="form-control" />
} }
@case ('hidden') { @case ('hidden') {
<input [formControlName]="prop.name" type="hidden" /> <input [formControlName]="prop().name" type="hidden" />
} }
@case ('checkbox') { @case ('checkbox') {
<div class="form-check" validationTarget> <div class="form-check" validationTarget>
<input #field [id]="prop.id" [formControlName]="prop.name" [abpDisabled]="disabled" type="checkbox" <input #field [id]="prop().id" [formControlName]="prop().name" [abpDisabled]="disabled" type="checkbox"
class="form-check-input" /> class="form-check-input" />
<ng-template [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: 'form-check-label' }" /> <ng-template [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: 'form-check-label' }" />
</div> </div>
} }
@case ('select') { @case ('select') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<select #field [id]="prop.id" [formControlName]="prop.name" [abpDisabled]="disabled" <select #field [id]="prop().id" [formControlName]="prop().name" [abpDisabled]="disabled"
class="form-select form-control"> class="form-select form-control">
@for (option of options$ | async; track option.value) { @for (option of options$ | async; track option.value) {
<option [ngValue]="option.value"> <option [ngValue]="option.value">
@if (prop.isExtra) { @if (prop().isExtra) {
{{ '::' + option.key | abpLocalization }} {{ '::' + option.key | abpLocalization }}
} @else { } @else {
{{ option.key }} {{ option.key }}
@ -39,42 +39,42 @@
} }
@case ('multiselect') { @case ('multiselect') {
<ng-template [ngTemplateOutlet]="label"></ng-template> <ng-template [ngTemplateOutlet]="label"></ng-template>
<abp-extensible-form-multi-select [prop]="prop" [options]="options$ | async" [formControlName]="prop.name" <abp-extensible-form-multi-select [prop]="prop()" [options]="options$ | async" [formControlName]="prop().name"
[abpDisabled]="disabled" /> [abpDisabled]="disabled" />
} }
@case ('typeahead') { @case ('typeahead') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<div #typeahead class="position-relative" validationStyle validationTarget> <div #typeahead class="position-relative" validationStyle validationTarget>
<input #field [id]="prop.id" [autocomplete]="prop.autocomplete" [abpDisabled]="disabled" [ngbTypeahead]="search" <input #field [id]="prop().id" [autocomplete]="prop().autocomplete" [abpDisabled]="disabled" [ngbTypeahead]="search"
[editable]="false" [inputFormatter]="typeaheadFormatter" [resultFormatter]="typeaheadFormatter" [editable]="false" [inputFormatter]="typeaheadFormatter" [resultFormatter]="typeaheadFormatter"
[ngModelOptions]="{ standalone: true }" [(ngModel)]="typeaheadModel" [ngModelOptions]="{ standalone: true }" [(ngModel)]="typeaheadModel"
(selectItem)="setTypeaheadValue($event.item)" (blur)="setTypeaheadValue(typeaheadModel)" (selectItem)="setTypeaheadValue($event.item)" (blur)="setTypeaheadValue(typeaheadModel)"
[class.is-invalid]="typeahead.classList.contains('is-invalid')" class="form-control" /> [class.is-invalid]="typeahead.classList.contains('is-invalid')" class="form-control" />
<input [formControlName]="prop.name" type="hidden" /> <input [formControlName]="prop().name" type="hidden" />
</div> </div>
} }
@case ('date') { @case ('date') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<input [id]="prop.id" [formControlName]="prop.name" (click)="datepicker.open()" (keyup.space)="datepicker.open()" <input [id]="prop().id" [formControlName]="prop().name" (click)="datepicker.open()" (keyup.space)="datepicker.open()"
ngbDatepicker #datepicker="ngbDatepicker" type="text" class="form-control" /> ngbDatepicker #datepicker="ngbDatepicker" type="text" class="form-control" />
} }
@case ('time') { @case ('time') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<ngb-timepicker [formControlName]="prop.name" /> <ngb-timepicker [formControlName]="prop().name" />
} }
@case ('dateTime') { @case ('dateTime') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<abp-extensible-date-time-picker [prop]="prop" [meridian]="meridian$ | async" /> <abp-extensible-date-time-picker [prop]="prop()" [meridian]="meridian$ | async" />
} }
@case ('textarea') { @case ('textarea') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<textarea #field [id]="prop.id" [formControlName]="prop.name" [abpDisabled]="disabled" [readonly]="readonly" <textarea #field [id]="prop().id" [formControlName]="prop().name" [abpDisabled]="disabled" [readonly]="readonly"
class="form-control"></textarea> class="form-control"></textarea>
} }
@case ('passwordinputgroup') { @case ('passwordinputgroup') {
<ng-template [ngTemplateOutlet]="label" /> <ng-template [ngTemplateOutlet]="label" />
<div class="input-group form-group" validationTarget> <div class="input-group form-group" validationTarget>
<input class="form-control" [id]="prop.id" [formControlName]="prop.name" [abpShowPassword]="showPassword" /> <input class="form-control" [id]="prop().id" [formControlName]="prop().name" [abpShowPassword]="showPassword" />
<button class="btn btn-secondary" type="button" (click)="showPassword = !showPassword"> <button class="btn btn-secondary" type="button" (click)="showPassword = !showPassword">
<i class="fa" aria-hidden="true" [class]="{ <i class="fa" aria-hidden="true" [class]="{
'fa-eye-slash': !showPassword, 'fa-eye-slash': !showPassword,
@ -85,27 +85,27 @@
} }
} }
@if (prop.formText) { @if (prop().formText) {
<small class="text-muted d-block">{{ prop.formText | abpLocalization }}</small> <small class="text-muted d-block">{{ prop().formText | abpLocalization }}</small>
} }
</div> </div>
</ng-container> </ng-container>
<ng-template #label let-classes> <ng-template #label let-classes>
<label [htmlFor]="prop.id" [class]="classes || 'form-label d-inline-block'"> <label [htmlFor]="prop().id" [class]="classes || 'form-label d-inline-block'">
<span class="d-inline-flex align-items-center gap-1 text-nowrap"> <span class="d-inline-flex align-items-center gap-1 text-nowrap">
@if (prop.displayTextResolver) { @if (prop().displayTextResolver) {
{{ prop.displayTextResolver(data()) | abpLocalization }} {{ prop().displayTextResolver(data()) | abpLocalization }}
} @else { } @else {
@if (prop.isExtra) { @if (prop().isExtra) {
{{ '::' + prop.displayName | abpLocalization }} {{ '::' + prop().displayName | abpLocalization }}
} @else { } @else {
{{ prop.displayName | abpLocalization }} {{ prop().displayName | abpLocalization }}
} }
} }
{{ asterisk }} {{ asterisk }}
@if (prop.tooltip) { @if (prop().tooltip) {
<i [ngbTooltip]="prop.tooltip.text | abpLocalization" [placement]="prop.tooltip.placement || 'auto'" <i [ngbTooltip]="prop().tooltip.text | abpLocalization" [placement]="prop().tooltip.placement || 'auto'"
container="body" class="bi bi-info-circle"></i> container="body" class="bi bi-info-circle"></i>
} }
</span> </span>

108
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -14,13 +14,11 @@ import {
ElementRef, ElementRef,
inject, inject,
Injector, Injector,
Input,
OnChanges, OnChanges,
Optional, Optional,
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild, ViewChild,
signal,
effect, effect,
input input
} from '@angular/core'; } from '@angular/core';
@ -43,7 +41,7 @@ import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { DateAdapter, DisabledDirective, TimeAdapter } from '@abp/ng.theme.shared'; import { DateAdapter, DisabledDirective, TimeAdapter } from '@abp/ng.theme.shared';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
import { FormProp } from '../../models/form-props'; import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props'; import { PropData, ReadonlyPropData } from '../../models/props';
import { selfFactory } from '../../utils/factory.util'; import { selfFactory } from '../../utils/factory.util';
import { addTypeaheadTextSuffix } from '../../utils/typeahead.util'; import { addTypeaheadTextSuffix } from '../../utils/typeahead.util';
import { eExtensibleComponents } from '../../enums/components'; import { eExtensibleComponents } from '../../enums/components';
@ -74,7 +72,7 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
NgComponentOutlet, NgComponentOutlet,
NgTemplateOutlet, NgTemplateOutlet,
FormsModule FormsModule
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ExtensibleFormPropService], providers: [ExtensibleFormPropService],
viewProviders: [ viewProviders: [
@ -87,7 +85,7 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
{ provide: NgbTimeAdapter, useClass: TimeAdapter }, { provide: NgbTimeAdapter, useClass: TimeAdapter },
], ],
}) })
export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit { export class ExtensibleFormPropComponent implements AfterViewInit {
protected service = inject(ExtensibleFormPropService); protected service = inject(ExtensibleFormPropService);
public readonly cdRef = inject(ChangeDetectorRef); public readonly cdRef = inject(ChangeDetectorRef);
public readonly track = inject(TrackByService); public readonly track = inject(TrackByService);
@ -96,9 +94,9 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
private readonly form = this.#groupDirective.form; private readonly form = this.#groupDirective.form;
readonly data = input.required<PropData>(); readonly data = input.required<PropData>();
@Input() prop!: FormProp; readonly prop = input.required<FormProp>();
readonly first = input<boolean>(undefined); readonly first = input<boolean | undefined>(undefined);
readonly isFirstGroup = input<boolean>(undefined); readonly isFirstGroup = input<boolean | undefined>(undefined);
@ViewChild('field') private fieldRef!: ElementRef<HTMLElement>; @ViewChild('field') private fieldRef!: ElementRef<HTMLElement>;
injectorForCustomComponent?: Injector; injectorForCustomComponent?: Injector;
@ -111,10 +109,55 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
typeaheadModel: any; typeaheadModel: any;
passwordKey = eExtensibleComponents.PasswordComponent; passwordKey = eExtensibleComponents.PasswordComponent;
disabledFn = (data: PropData) => false; disabledFn = (data: ReadonlyPropData) => false;
get disabled() { get disabled() {
return this.disabledFn(this.data()); return this.disabledFn(this.data().data);
}
constructor() {
// Watch prop changes and update state
effect(() => {
const currentProp = this.prop();
if (!currentProp) return;
const { options, readonly, disabled, validators, className, template } = currentProp;
if (template) {
this.injectorForCustomComponent = Injector.create({
providers: [
{
provide: EXTENSIONS_FORM_PROP,
useValue: currentProp,
},
{
provide: EXTENSIONS_FORM_PROP_DATA,
useValue: this.data()?.data?.record,
},
{ provide: ControlContainer, useExisting: FormGroupDirective },
],
parent: this.injector,
});
}
if (options) this.options$ = options(this.data().data);
if (readonly) this.readonly = readonly(this.data().data);
if (disabled) {
this.disabledFn = disabled;
}
if (validators) {
this.validators = validators(this.data().data);
this.setAsterisk();
}
if (className !== undefined) {
this.containerClassName = className;
}
const [keyControl, valueControl] = this.getTypeaheadControls();
if (keyControl && valueControl)
this.typeaheadModel = { key: keyControl.value, value: valueControl.value };
});
} }
setTypeaheadValue(selectedOption: ABP.Option<string>) { setTypeaheadValue(selectedOption: ABP.Option<string>) {
@ -131,7 +174,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
? text$.pipe( ? text$.pipe(
debounceTime(300), debounceTime(300),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(text => this.prop?.options?.(this.data(), text) || of([])), switchMap(text => this.prop()?.options?.(this.data().data, text) || of([])),
) )
: of([]); : of([]);
@ -140,12 +183,12 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
meridian$ = this.service.meridian$; meridian$ = this.service.meridian$;
get isInvalid() { get isInvalid() {
const control = this.form.get(this.prop.name); const control = this.form.get(this.prop().name);
return control?.touched && control.invalid; return control?.touched && control.invalid;
} }
private getTypeaheadControls() { private getTypeaheadControls() {
const { name } = this.prop; const { name } = this.prop();
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`; const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`;
const keyControl = const keyControl =
this.form.get(addTypeaheadTextSuffix(extraPropName)) || this.form.get(addTypeaheadTextSuffix(extraPropName)) ||
@ -173,43 +216,4 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
getType(prop: FormProp): string { getType(prop: FormProp): string {
return this.service.getType(prop); return this.service.getType(prop);
} }
ngOnChanges({ prop, data }: SimpleChanges) {
const currentProp = prop?.currentValue as FormProp;
const { options, readonly, disabled, validators, className, template } = currentProp || {};
if (template) {
this.injectorForCustomComponent = Injector.create({
providers: [
{
provide: EXTENSIONS_FORM_PROP,
useValue: currentProp,
},
{
provide: EXTENSIONS_FORM_PROP_DATA,
useValue: (data?.currentValue as PropData)?.record,
},
{ provide: ControlContainer, useExisting: FormGroupDirective },
],
parent: this.injector,
});
}
if (options) this.options$ = options(this.data());
if (readonly) this.readonly = readonly(this.data());
if (disabled) {
this.disabledFn = disabled;
}
if (validators) {
this.validators = validators(this.data());
this.setAsterisk();
}
if (className !== undefined) {
this.containerClassName = className;
}
const [keyControl, valueControl] = this.getTypeaheadControls();
if (keyControl && valueControl)
this.typeaheadModel = { key: keyControl.value, value: valueControl.value };
}
} }

4
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html

@ -1,6 +1,6 @@
@if (form) { @if (form) {
@for (groupedProp of groupedPropList.items; track i; let i = $index; let first = $first) { @for (groupedProp of groupedPropList()?.items; track i; let i = $index; let first = $first) {
<ng-container *abpPropData="let data; fromList: groupedProp.formPropList; withRecord: record"> <ng-container *abpPropData="let data; fromList: groupedProp.formPropList; withRecord: record()">
@if (isAnyGroupMemberVisible(i, data) && groupedProp.group?.className) { @if (isAnyGroupMemberVisible(i, data) && groupedProp.group?.className) {
<div [class]="groupedProp.group?.className" <div [class]="groupedProp.group?.className"
[attr.data-name]="groupedProp.group?.name || groupedProp.group?.className"> [attr.data-name]="groupedProp.group?.name || groupedProp.group?.className">

32
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts

@ -4,11 +4,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
inject, inject,
Input,
Optional, Optional,
QueryList, QueryList,
SkipSelf, SkipSelf,
ViewChildren, ViewChildren,
input,
signal,
effect
} from '@angular/core'; } from '@angular/core';
import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
@ -44,18 +46,22 @@ export class ExtensibleFormComponent<R = any> {
@ViewChildren(ExtensibleFormPropComponent) @ViewChildren(ExtensibleFormPropComponent)
formProps!: QueryList<ExtensibleFormPropComponent>; formProps!: QueryList<ExtensibleFormPropComponent>;
@Input() readonly selectedRecord = input<R | undefined>(undefined);
set selectedRecord(record: R) {
const type = !record || JSON.stringify(record) === '{}' ? 'create' : 'edit';
const propList = this.extensions[`${type}FormProps`].get(this.identifier).props;
this.groupedPropList = this.createGroupedList(propList);
this.record = record;
}
extraPropertiesKey = EXTRA_PROPERTIES_KEY; extraPropertiesKey = EXTRA_PROPERTIES_KEY;
groupedPropList!: GroupedFormPropList; readonly groupedPropList = signal<GroupedFormPropList | undefined>(undefined);
groupedPropListOfArray: FormProp<any>[][]; groupedPropListOfArray: FormProp<any>[][];
record!: R; readonly record = signal<R | undefined>(undefined);
constructor() {
effect(() => {
const recordValue = this.selectedRecord();
const type = !recordValue || JSON.stringify(recordValue) === '{}' ? 'create' : 'edit';
const propList = this.extensions[`${type}FormProps`].get(this.identifier).props;
this.groupedPropList.set(this.createGroupedList(propList));
this.record.set(recordValue);
});
}
get form(): UntypedFormGroup { get form(): UntypedFormGroup {
return (this.container ? this.container.control : { controls: {} }) as UntypedFormGroup; return (this.container ? this.container.control : { controls: {} }) as UntypedFormGroup;
@ -75,8 +81,10 @@ export class ExtensibleFormComponent<R = any> {
} }
//TODO: Reactor this method //TODO: Reactor this method
isAnyGroupMemberVisible(index: number, data) { isAnyGroupMemberVisible(index: number, data: any) {
const { items } = this.groupedPropList; const groupedPropListValue = this.groupedPropList();
if (!groupedPropListValue) return false;
const { items } = groupedPropListValue;
const formPropList = items[index].formPropList.toArray(); const formPropList = items[index].formPropList.toArray();
return formPropList.some(prop => prop.visible(data)); return formPropList.some(prop => prop.visible(data));
} }

18
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html

@ -1,6 +1,6 @@
@if (isBrowser) { @if (isBrowser) {
<ngx-datatable #table default [rows]="data" [count]="recordsTotal()" [list]="list()" <ngx-datatable #table default [rows]="data" [count]="recordsTotal()" [list]="list()"
[selectionType]="selectable() ? _selectionType : undefined" (activate)="tableActivate.emit($event)" [selectionType]="selectable() ? selectionType() : undefined" (activate)="tableActivate.emit($event)"
(select)="onSelect($event)" [selected]="selected()" (scroll)="onScroll($event)" [scrollbarV]="infiniteScroll()" (select)="onSelect($event)" [selected]="selected()" (scroll)="onScroll($event)" [scrollbarV]="infiniteScroll()"
[style.height]="getTableHeight()" [loadingIndicator]="infiniteScroll() && isLoading()" [style.height]="getTableHeight()" [loadingIndicator]="infiniteScroll() && isLoading()"
[footerHeight]="infiniteScroll() ? false : 50"> [footerHeight]="infiniteScroll() ? false : 50">
@ -28,7 +28,7 @@
<ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected" <ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn"> let-selectFn="selectFn">
@if (_selectionType !== 'single') { @if (selectionType() !== 'single') {
<div class="form-check"> <div class="form-check">
<input class="form-check-input table-check" type="checkbox" [checked]="allRowsSelected" <input class="form-check-input table-check" type="checkbox" [checked]="allRowsSelected"
(change)="selectFn(!allRowsSelected)" /> (change)="selectFn(!allRowsSelected)" />
@ -38,12 +38,12 @@
<ng-template ngx-datatable-cell-template let-value="value" let-row="row" let-isSelected="isSelected" <ng-template ngx-datatable-cell-template let-value="value" let-row="row" let-isSelected="isSelected"
let-onCheckboxChangeFn="onCheckboxChangeFn"> let-onCheckboxChangeFn="onCheckboxChangeFn">
@if(_selectionType === 'single') { @if(selectionType() === 'single') {
<div class="h-100 form-check form-check-sm form-check-custom form-check-solid"> <div class="h-100 form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="radio" [checked]="isSelected" (change)="onCheckboxChangeFn($event)" /> <input class="form-check-input" type="radio" [checked]="isSelected" (change)="onCheckboxChangeFn($event)" />
</div> </div>
} }
@if (_selectionType !== 'single') { @if (selectionType() !== 'single') {
<div class="h-100 form-check form-check-sm form-check-custom form-check-solid"> <div class="h-100 form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" [checked]="isSelected" (change)="onCheckboxChangeFn($event)" /> <input class="form-check-input" type="checkbox" [checked]="isSelected" (change)="onCheckboxChangeFn($event)" />
</div> </div>
@ -52,12 +52,12 @@
</ngx-datatable-column> </ngx-datatable-column>
} }
@if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) { @if (actionsTemplate() || (actionList.length && hasAtLeastOnePermittedAction)) {
<ngx-datatable-column [name]="actionsText | abpLocalization" [maxWidth]="_actionsColumnWidth() ?? undefined" <ngx-datatable-column [name]="actionsText() | abpLocalization" [maxWidth]="_actionsColumnWidth() ?? undefined"
[width]="_actionsColumnWidth() ?? 200" [canAutoResize]="!_actionsColumnWidth()" [sortable]="false"> [width]="_actionsColumnWidth() ?? 200" [canAutoResize]="!_actionsColumnWidth()" [sortable]="false">
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template> <ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container <ng-container
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }"></ng-container> *ngTemplateOutlet="actionsTemplate() || gridActions; context: { $implicit: row, index: i }"></ng-container>
<ng-template #gridActions> <ng-template #gridActions>
@if (isVisibleActions(row)) { @if (isVisibleActions(row)) {
<abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions> <abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions>
@ -67,8 +67,8 @@
</ngx-datatable-column> </ngx-datatable-column>
} }
@for (prop of propList; track prop.name; let i = $index) { @for (prop of propList; track prop.name; let i = $index) {
<ngx-datatable-column *abpVisible="prop.columnVisible(getInjected)" [width]="columnWidths[i] ?? 200" <ngx-datatable-column *abpVisible="prop.columnVisible(getInjected)" [width]="columnWidths()[i] ?? 200"
[canAutoResize]="!columnWidths[i]" [canAutoResize]="!columnWidths()[i]"
[name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization" [prop]="prop.name" [name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization" [prop]="prop.name"
[sortable]="prop.sortable"> [sortable]="prop.sortable">
<ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn"> <ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn">

180
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -5,21 +5,18 @@ import {
Component, Component,
computed, computed,
ContentChild, ContentChild,
EventEmitter,
inject, inject,
Injector, Injector,
Input,
LOCALE_ID, LOCALE_ID,
OnChanges,
OnDestroy, OnDestroy,
Output,
PLATFORM_ID, PLATFORM_ID,
signal, signal,
SimpleChanges,
TemplateRef, TemplateRef,
TrackByFunction, TrackByFunction,
ViewChild, ViewChild,
input input,
effect,
output
} from '@angular/core'; } from '@angular/core';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
@ -47,7 +44,7 @@ import {
import { ePropType } from '../../enums/props.enum'; import { ePropType } from '../../enums/props.enum';
import { EntityActionList } from '../../models/entity-actions'; import { EntityActionList } from '../../models/entity-actions';
import { EntityProp, EntityPropList } from '../../models/entity-props'; import { EntityProp, EntityPropList } from '../../models/entity-props';
import { PropData } from '../../models/props'; import { ReadonlyPropData } from '../../models/props';
import { ExtensionsService } from '../../services/extensions.service'; import { ExtensionsService } from '../../services/extensions.service';
import { import {
ENTITY_PROP_TYPE_CLASSES, ENTITY_PROP_TYPE_CLASSES,
@ -87,7 +84,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
} }
`], `],
}) })
export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewInit, OnDestroy { export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestroy {
readonly #injector = inject(Injector); readonly #injector = inject(Injector);
readonly getInjected = this.#injector.get.bind(this.#injector); readonly getInjected = this.#injector.get.bind(this.#injector);
protected readonly cdr = inject(ChangeDetectorRef); protected readonly cdr = inject(ChangeDetectorRef);
@ -99,54 +96,56 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
private platformId = inject(PLATFORM_ID); private platformId = inject(PLATFORM_ID);
protected isBrowser = isPlatformBrowser(this.platformId); protected isBrowser = isPlatformBrowser(this.platformId);
protected _actionsText!: string; // Input signals
@Input() readonly actionsTextInput = input<string | undefined>(undefined, { alias: 'actionsText' });
set actionsText(value: string) { readonly dataInput = input<R[]>([], { alias: 'data' });
this._actionsText = value;
}
get actionsText(): string {
return this._actionsText ?? (this.actionList.length >= 1 ? 'AbpUi::Actions' : '');
}
@Input() data!: R[];
readonly list = input.required<ListService>(); readonly list = input.required<ListService>();
readonly recordsTotal = input.required<number>(); readonly recordsTotal = input.required<number>();
readonly actionsColumnWidthInput = input<number | undefined>(undefined, { alias: 'actionsColumnWidth' });
@Input() set actionsColumnWidth(width: number) { readonly actionsTemplate = input<TemplateRef<any> | undefined>(undefined);
this._actionsColumnWidth.set(width ? Number(width) : undefined);
}
@Input() actionsTemplate?: TemplateRef<any>;
@Output() tableActivate = new EventEmitter();
readonly selectable = input(false); readonly selectable = input(false);
readonly selectionTypeInput = input<SelectionType | string>(SelectionType.multiClick, { alias: 'selectionType' });
@Input() set selectionType(value: SelectionType | string) {
this._selectionType = typeof value === 'string' ? SelectionType[value] : value;
}
_selectionType: SelectionType = SelectionType.multiClick;
readonly selected = input<any[]>([]); readonly selected = input<any[]>([]);
@Output() selectionChange = new EventEmitter<any[]>();
// Infinite scroll configuration
readonly infiniteScroll = input(false); readonly infiniteScroll = input(false);
readonly isLoading = input(false); readonly isLoading = input(false);
readonly scrollThreshold = input(10); readonly scrollThreshold = input(10);
@Output() loadMore = new EventEmitter<void>(); readonly tableHeight = input<number | undefined>(undefined);
readonly tableHeight = input<number>(undefined); readonly rowDetailTemplate = input<TemplateRef<RowDetailContext<R>> | undefined>(undefined);
readonly rowDetailTemplate = input<TemplateRef<RowDetailContext<R>>>(undefined);
readonly rowDetailHeight = input<string | number>('100%'); readonly rowDetailHeight = input<string | number>('100%');
@Output() rowDetailToggle = new EventEmitter<R>();
// Output signals
readonly tableActivate = output<any>();
readonly selectionChange = output<any[]>();
readonly loadMore = output<void>();
readonly rowDetailToggle = output<R>();
// Internal signals
protected readonly _data = signal<R[]>([]);
private readonly _actionsColumnWidth = signal<number | undefined>(DEFAULT_ACTIONS_COLUMN_WIDTH);
@ContentChild(ExtensibleTableRowDetailComponent) @ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>; rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@ViewChild('table', { static: false }) table!: DatatableComponent; @ViewChild('table', { static: false }) table!: DatatableComponent;
// Computed values
protected readonly actionsText = computed(() => {
return this.actionsTextInput() ?? (this.actionList.length >= 1 ? 'AbpUi::Actions' : '');
});
protected readonly selectionType = computed(() => {
const value = this.selectionTypeInput();
return typeof value === 'string' ? SelectionType[value as keyof typeof SelectionType] : value;
});
protected get data(): R[] {
return this._data();
}
protected set data(value: R[]) {
this._data.set(value);
}
protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined { protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate(); return this.rowDetailComponent?.template() ?? this.rowDetailTemplate();
} }
@ -163,9 +162,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name; readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name;
// Signal for actions column width
private readonly _actionsColumnWidth = signal<number | undefined>(DEFAULT_ACTIONS_COLUMN_WIDTH);
// Infinite scroll: debounced load more subject // Infinite scroll: debounced load more subject
private readonly loadMoreSubject = new Subject<void>(); private readonly loadMoreSubject = new Subject<void>();
private readonly loadMoreSubscription = this.loadMoreSubject private readonly loadMoreSubscription = this.loadMoreSubject
@ -187,6 +183,53 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
this.permissionService.filterItemsByPolicy( this.permissionService.filterItemsByPolicy(
this.actionList.toArray().map(action => ({ requiredPolicy: action.permission })), this.actionList.toArray().map(action => ({ requiredPolicy: action.permission })),
).length > 0; ).length > 0;
// Watch actionsColumnWidth input
effect(() => {
const width = this.actionsColumnWidthInput();
this._actionsColumnWidth.set(width ? Number(width) : undefined);
});
// Watch data input changes
effect(() => {
const dataValue = this.dataInput();
if (!dataValue) return;
if (dataValue.length < 1) {
this.list().totalCount = this.recordsTotal();
}
this._data.set(dataValue.map((record: any, index: number) => {
this.propList.forEach(prop => {
const propData = { getInjected: this.getInjected, record, index } as ReadonlyPropData;
const value = this.getContent(prop.value, propData);
const propKey = `_${prop.value.name}`;
record[propKey] = {
visible: prop.value.visible(propData),
value,
};
if (prop.value.component) {
record[propKey].injector = Injector.create({
providers: [
{
provide: PROP_DATA_STREAM,
useValue: value,
},
{
provide: ROW_RECORD,
useValue: record,
},
],
parent: this.#injector,
});
record[propKey].component = prop.value.component;
}
});
return record;
}));
});
} }
private getIcon(value: boolean) { private getIcon(value: boolean) {
@ -201,7 +244,7 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return key; return key;
} }
getContent(prop: EntityProp<R>, data: PropData): Observable<string> { getContent(prop: EntityProp<R>, data: ReadonlyPropData): Observable<string> {
return prop.valueResolver(data).pipe( return prop.valueResolver(data).pipe(
map(value => { map(value => {
switch (prop.type) { switch (prop.type) {
@ -217,45 +260,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
); );
} }
ngOnChanges({ data }: SimpleChanges) {
if (!data?.currentValue) return;
if (data.currentValue.length < 1) {
this.list().totalCount = this.recordsTotal();
}
this.data = data.currentValue.map((record: any, index: number) => {
this.propList.forEach(prop => {
const propData = { getInjected: this.getInjected, record, index } as any;
const value = this.getContent(prop.value, propData);
const propKey = `_${prop.value.name}`;
record[propKey] = {
visible: prop.value.visible(propData),
value,
};
if (prop.value.component) {
record[propKey].injector = Injector.create({
providers: [
{
provide: PROP_DATA_STREAM,
useValue: value,
},
{
provide: ROW_RECORD,
useValue: record,
},
],
parent: this.#injector,
});
record[propKey].component = prop.value.component;
}
});
return record;
});
}
isVisibleActions(rowData: any): boolean { isVisibleActions(rowData: any): boolean {
const actions = this.actionList.toArray(); const actions = this.actionList.toArray();
const visibleActions = actions.filter(action => { const visibleActions = actions.filter(action => {
@ -278,9 +282,9 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return visibleActions.length > 0; return visibleActions.length > 0;
} }
onSelect({ selected }) { onSelect({ selected }: { selected: any[] }) {
const selectedValue = this.selected(); const selectedValue = this.selected();
this.selected().splice(0, selectedValue.length); selectedValue.splice(0, selectedValue.length);
selectedValue.push(...selected); selectedValue.push(...selected);
this.selectionChange.emit(selected); this.selectionChange.emit(selected);
} }
@ -330,7 +334,7 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (!this.infiniteScroll()) { if (!this.infiniteScroll()) {
this.list()?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { this.list()?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => {
this.data = []; this._data.set([]);
this.cdr.markForCheck(); this.cdr.markForCheck();
}); });
} }

5
npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts

@ -1,7 +1,6 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
Input,
TrackByFunction, TrackByFunction,
input input
} from '@angular/core'; } from '@angular/core';
@ -35,9 +34,7 @@ import { NgTemplateOutlet } from '@angular/common';
}) })
export class GridActionsComponent<R = any> extends AbstractActionsComponent<EntityActionList<R>> { export class GridActionsComponent<R = any> extends AbstractActionsComponent<EntityActionList<R>> {
readonly icon = input('fa fa-cog'); readonly icon = input('fa fa-cog');
readonly index = input<number | undefined>(undefined);
@Input() readonly index?: number;
readonly text = input(''); readonly text = input('');
readonly trackByFn: TrackByFunction<EntityAction<R>> = (_, item) => item.text; readonly trackByFn: TrackByFunction<EntityAction<R>> = (_, item) => item.text;

41
npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts

@ -2,34 +2,33 @@
import { import {
Directive, Directive,
Injector, Injector,
Input,
OnChanges,
OnDestroy, OnDestroy,
TemplateRef, TemplateRef,
ViewContainerRef, ViewContainerRef,
effect,
inject, inject,
input input
} from '@angular/core'; } from '@angular/core';
import { PropData, PropList } from '../models/props'; import { PropData, PropList } from '../models/props';
type InferredRecord<L> = L extends PropList<infer R> ? R : never;
@Directive({ @Directive({
exportAs: 'abpPropData', exportAs: 'abpPropData',
selector: '[abpPropData]', selector: '[abpPropData]',
}) })
export class PropDataDirective<L extends PropList<any>> export class PropDataDirective<L extends PropList<any>>
extends PropData<InferredData<L>> extends PropData<InferredRecord<L>>
implements OnChanges, OnDestroy implements OnDestroy
{ {
private tempRef = inject<TemplateRef<any>>(TemplateRef); private tempRef = inject<TemplateRef<any>>(TemplateRef);
private vcRef = inject(ViewContainerRef); private vcRef = inject(ViewContainerRef);
readonly propList = input<L>(undefined, { alias: "abpPropDataFromList" }); readonly propList = input<L | undefined>(undefined, { alias: 'abpPropDataFromList' });
readonly record = input.required<InferredRecord<L>>({ alias: 'abpPropDataWithRecord' });
@Input('abpPropDataWithRecord') record!: InferredData<L>['record']; readonly index = input<number | undefined>(undefined, { alias: 'abpPropDataAtIndex' });
@Input('abpPropDataAtIndex') index?: number; readonly getInjected: PropData<InferredRecord<L>>['getInjected'];
readonly getInjected: InferredData<L>['getInjected'];
constructor() { constructor() {
const injector = inject(Injector); const injector = inject(Injector);
@ -37,14 +36,19 @@ export class PropDataDirective<L extends PropList<any>>
super(); super();
this.getInjected = injector.get.bind(injector); this.getInjected = injector.get.bind(injector);
}
ngOnChanges() { // Watch for input changes and re-render
this.vcRef.clear(); effect(() => {
// Read all inputs to track them
this.vcRef.createEmbeddedView(this.tempRef, { this.record();
$implicit: this.data, this.index();
index: 0, this.propList();
this.vcRef.clear();
this.vcRef.createEmbeddedView(this.tempRef, {
$implicit: this.data,
index: 0,
});
}); });
} }
@ -52,6 +56,3 @@ export class PropDataDirective<L extends PropList<any>>
this.vcRef.clear(); this.vcRef.clear();
} }
} }
type InferredData<L> = PropData<InferredRecord<L>>;
type InferredRecord<L> = L extends PropList<infer R> ? R : never;

5
npm/ng-packs/packages/components/extensible/src/lib/models/actions.ts

@ -9,18 +9,19 @@ export abstract class ActionData<R = any> {
notFoundValue?: T, notFoundValue?: T,
flags?: InjectOptions, flags?: InjectOptions,
) => T; ) => T;
index?: number; index?: number | InputSignal<number | undefined>;
abstract record: R | InputSignal<R>; abstract record: R | InputSignal<R>;
get data(): ReadonlyActionData<R> { get data(): ReadonlyActionData<R> {
return { return {
getInjected: this.getInjected, getInjected: this.getInjected,
index: this.index, index: isSignal(this.index) ? this.index() : this.index,
record: isSignal(this.record) ? this.record() : this.record, record: isSignal(this.record) ? this.record() : this.record,
}; };
} }
} }
export type ReadonlyActionData<R = any> = Readonly<{ export type ReadonlyActionData<R = any> = Readonly<{
getInjected: <T>( getInjected: <T>(
token: Type<T> | InjectionToken<T>, token: Type<T> | InjectionToken<T>,

27
npm/ng-packs/packages/components/extensible/src/lib/models/props.ts

@ -1,4 +1,4 @@
import { InjectionToken, InjectOptions, Type } from '@angular/core'; import { InjectionToken, InjectOptions, InputSignal, isSignal, Type } from '@angular/core';
import { LinkedList } from '@abp/utils'; import { LinkedList } from '@abp/utils';
import { ePropType } from '../enums/props.enum'; import { ePropType } from '../enums/props.enum';
import { FormPropTooltip } from './form-props'; import { FormPropTooltip } from './form-props';
@ -11,19 +11,27 @@ export abstract class PropData<R = any> {
notFoundValue?: T, notFoundValue?: T,
options?: InjectOptions, options?: InjectOptions,
) => T; ) => T;
index?: number; index?: number | InputSignal<number | undefined>;
abstract record: R; abstract record: R | InputSignal<R>;
get data(): ReadonlyPropData<R> { get data(): ReadonlyPropData<R> {
return { return {
getInjected: this.getInjected, getInjected: this.getInjected,
index: this.index, index: isSignal(this.index) ? this.index() : this.index,
record: this.record, record: isSignal(this.record) ? this.record() : this.record,
}; };
} }
} }
export type ReadonlyPropData<R = any> = Readonly<Omit<PropData<R>, 'data'>>; export type ReadonlyPropData<R = any> = Readonly<{
getInjected: <T>(
token: Type<T> | InjectionToken<T>,
notFoundValue?: T,
options?: InjectOptions,
) => T;
index?: number;
record: R;
}>;
export abstract class Prop<R = any> { export abstract class Prop<R = any> {
constructor( constructor(
@ -43,9 +51,10 @@ export abstract class Prop<R = any> {
} }
} }
export type PropCallback<T, R = any> = (data: Omit<PropData<T>, 'data'>, auxData?: any) => R; export type PropCallback<T, R = any> = (data: ReadonlyPropData<T>, auxData?: any) => R;
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>, auxData?: any) => boolean; export type PropPredicate<T> = (data?: ReadonlyPropData<T>, auxData?: any) => boolean;
export type PropDisplayTextResolver<T> = (data?: Omit<PropData<T>, 'data'>) => string; export type PropDisplayTextResolver<T> = (data?: ReadonlyPropData<T>) => string;
export abstract class PropsFactory<C extends Props<any>> { export abstract class PropsFactory<C extends Props<any>> {
protected abstract _ctor: Type<C>; protected abstract _ctor: Type<C>;

10
npm/ng-packs/packages/components/extensible/src/lib/utils/form-props.util.ts

@ -4,11 +4,14 @@ import { DateTimeAdapter, DateAdapter, TimeAdapter } from '@abp/ng.theme.shared'
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties';
import { ePropType } from '../enums/props.enum'; import { ePropType } from '../enums/props.enum';
import { FormPropList } from '../models/form-props'; import { FormPropList } from '../models/form-props';
import { PropData } from '../models/props'; import { PropData, ReadonlyPropData } from '../models/props';
import { ExtensionsService } from '../services/extensions.service'; import { ExtensionsService } from '../services/extensions.service';
import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token'; import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token';
export function generateFormFromProps<R = any>(data: PropData<R>) { export function generateFormFromProps<R = any>(propData: PropData<R>) {
// Use .data to get resolved values (handles InputSignal)
const data: ReadonlyPropData<R> = propData.data;
const extensions = data.getInjected(ExtensionsService<R>); const extensions = data.getInjected(ExtensionsService<R>);
const identifier = data.getInjected(EXTENSIONS_IDENTIFIER); const identifier = data.getInjected(EXTENSIONS_IDENTIFIER);
@ -16,7 +19,7 @@ export function generateFormFromProps<R = any>(data: PropData<R>) {
const extraForm = new UntypedFormGroup({}); const extraForm = new UntypedFormGroup({});
form.addControl(EXTRA_PROPERTIES_KEY, extraForm); form.addControl(EXTRA_PROPERTIES_KEY, extraForm);
const record = data.record || {}; const record: any = data.record || {};
const type = JSON.stringify(record) === '{}' ? 'create' : 'edit'; const type = JSON.stringify(record) === '{}' ? 'create' : 'edit';
const props: FormPropList<R> = extensions[`${type}FormProps`].get(identifier).props; const props: FormPropList<R> = extensions[`${type}FormProps`].get(identifier).props;
const extraProperties = record[EXTRA_PROPERTIES_KEY] || {}; const extraProperties = record[EXTRA_PROPERTIES_KEY] || {};
@ -65,3 +68,4 @@ export function generateFormFromProps<R = any>(data: PropData<R>) {
return form; return form;
} }

45
npm/ng-packs/packages/components/page/src/page-part.directive.ts

@ -2,16 +2,13 @@ import {
Directive, Directive,
TemplateRef, TemplateRef,
ViewContainerRef, ViewContainerRef,
Input,
InjectionToken, InjectionToken,
OnInit, OnInit,
OnDestroy, OnDestroy,
Injector, Injector,
OnChanges,
SimpleChanges,
SimpleChange,
inject, inject,
input input,
effect
} from '@angular/core'; } from '@angular/core';
import { Observable, Subscription, of } from 'rxjs'; import { Observable, Subscription, of } from 'rxjs';
@ -19,7 +16,7 @@ export interface PageRenderStrategy {
shouldRender(type?: string): boolean | Observable<boolean>; shouldRender(type?: string): boolean | Observable<boolean>;
onInit?(type?: string, injector?: Injector, context?: any): void; onInit?(type?: string, injector?: Injector, context?: any): void;
onDestroy?(type?: string, injector?: Injector, context?: any): void; onDestroy?(type?: string, injector?: Injector, context?: any): void;
onContextUpdate?(change?: SimpleChange): void; onContextUpdate?(context?: any): void;
} }
export const PAGE_RENDER_STRATEGY = new InjectionToken<PageRenderStrategy>('PAGE_RENDER_STRATEGY'); export const PAGE_RENDER_STRATEGY = new InjectionToken<PageRenderStrategy>('PAGE_RENDER_STRATEGY');
@ -27,20 +24,34 @@ export const PAGE_RENDER_STRATEGY = new InjectionToken<PageRenderStrategy>('PAGE
@Directive({ @Directive({
selector: '[abpPagePart]', selector: '[abpPagePart]',
}) })
export class PagePartDirective implements OnInit, OnDestroy, OnChanges { export class PagePartDirective implements OnInit, OnDestroy {
private templateRef = inject<TemplateRef<any>>(TemplateRef); private templateRef = inject<TemplateRef<any>>(TemplateRef);
private viewContainer = inject(ViewContainerRef); private viewContainer = inject(ViewContainerRef);
private renderLogic = inject<PageRenderStrategy>(PAGE_RENDER_STRATEGY, { optional: true })!; private renderLogic = inject<PageRenderStrategy>(PAGE_RENDER_STRATEGY, { optional: true })!;
private injector = inject(Injector); private injector = inject(Injector);
hasRendered = false; hasRendered = false;
type!: string;
subscription!: Subscription; subscription!: Subscription;
readonly context = input<any>(undefined, { alias: "abpPagePartContext" }); readonly context = input<any>(undefined, { alias: 'abpPagePartContext' });
@Input() set abpPagePart(type: string) { readonly abpPagePart = input<string>('');
this.type = type;
this.createRenderStream(type); constructor() {
// Watch for type changes
effect(() => {
const type = this.abpPagePart();
if (type) {
this.createRenderStream(type);
}
});
// Watch for context changes
effect(() => {
const ctx = this.context();
if (this.renderLogic?.onContextUpdate) {
this.renderLogic.onContextUpdate(ctx);
}
});
} }
render = (shouldRender: boolean) => { render = (shouldRender: boolean) => {
@ -53,15 +64,9 @@ export class PagePartDirective implements OnInit, OnDestroy, OnChanges {
} }
}; };
ngOnChanges({ context }: SimpleChanges): void {
if (this.renderLogic?.onContextUpdate) {
this.renderLogic.onContextUpdate(context);
}
}
ngOnInit() { ngOnInit() {
if (this.renderLogic?.onInit) { if (this.renderLogic?.onInit) {
this.renderLogic.onInit(this.type, this.injector, this.context()); this.renderLogic.onInit(this.abpPagePart(), this.injector, this.context());
} }
} }
@ -69,7 +74,7 @@ export class PagePartDirective implements OnInit, OnDestroy, OnChanges {
this.clearSubscription(); this.clearSubscription();
if (this.renderLogic?.onDestroy) { if (this.renderLogic?.onDestroy) {
this.renderLogic.onDestroy(this.type, this.injector, this.context()); this.renderLogic.onDestroy(this.abpPagePart(), this.injector, this.context());
} }
} }

Loading…
Cancel
Save