Browse Source

Merge d27539af8c into 42324fed75

pull/24777/merge
Fahri Gedik 2 days ago
committed by GitHub
parent
commit
ed0b034aa8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 97
      npm/ng-packs/packages/components/chart.js/src/chart.component.ts
  2. 5
      npm/ng-packs/packages/components/extensible/src/lib/components/abstract-actions/abstract-actions.component.ts
  3. 54
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html
  4. 117
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  5. 4
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html
  6. 32
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts
  7. 309
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  8. 237
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  9. 2
      npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html
  10. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts
  11. 56
      npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts
  12. 25
      npm/ng-packs/packages/components/extensible/src/lib/models/actions.ts
  13. 27
      npm/ng-packs/packages/components/extensible/src/lib/models/props.ts
  14. 10
      npm/ng-packs/packages/components/extensible/src/lib/utils/form-props.util.ts
  15. 64
      npm/ng-packs/packages/components/page/src/page-part.directive.ts
  16. 12
      npm/ng-packs/packages/components/page/src/page.component.html
  17. 36
      npm/ng-packs/packages/components/page/src/page.component.ts
  18. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html
  19. 121
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts
  20. 21
      npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
  21. 15
      npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts
  22. 6
      npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts
  23. 63
      npm/ng-packs/packages/core/src/lib/directives/for.directive.ts
  24. 31
      npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts
  25. 10
      npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts
  26. 70
      npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts
  27. 15
      npm/ng-packs/packages/core/src/lib/directives/show-password.directive.ts
  28. 6
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html
  29. 69
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts
  30. 34
      npm/ng-packs/packages/feature-management/src/lib/directives/free-text-input.directive.ts
  31. 6
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html
  32. 137
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  33. 2
      npm/ng-packs/packages/setting-management/package.json
  34. 6
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html
  35. 4
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts
  36. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.html
  37. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts
  38. 97
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  39. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts
  40. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts
  41. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts
  42. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts
  43. 41
      npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts
  44. 45
      npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts
  45. 45
      npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts
  46. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.html
  47. 6
      npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts
  48. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
  49. 41
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts
  50. 6
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.html
  51. 20
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts
  52. 5
      npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts
  53. 59
      npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts
  54. 73
      npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts
  55. 6
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts
  56. 33
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts
  57. 32
      npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts

97
npm/ng-packs/packages/components/chart.js/src/chart.component.ts

@ -1,17 +1,17 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
ViewChild,
inject
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
OnDestroy,
Output,
ViewChild,
effect,
inject,
input,
untracked,
} from '@angular/core';
let Chart: any;
@ -21,13 +21,13 @@ let Chart: any;
template: `
<div
style="position:relative"
[style.width]="responsive && !width ? null : width"
[style.height]="responsive && !height ? null : height"
[style.width]="responsive() && !width() ? null : width()"
[style.height]="responsive() && !height() ? null : height()"
>
<canvas
#canvas
[attr.width]="responsive && !width ? null : width"
[attr.height]="responsive && !height ? null : height"
[attr.width]="responsive() && !width() ? null : width()"
[attr.height]="responsive() && !height() ? null : height()"
(click)="onCanvasClick($event)"
></canvas>
</div>
@ -35,32 +35,39 @@ let Chart: any;
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs: 'abpChart',
})
export class ChartComponent implements AfterViewInit, OnDestroy, OnChanges {
export class ChartComponent implements AfterViewInit, OnDestroy {
el = inject(ElementRef);
private cdr = inject(ChangeDetectorRef);
@Input() type!: string;
@Input() data: any = {};
@Input() options: any = {};
@Input() plugins: any[] = [];
@Input() width?: string;
@Input() height?: string;
@Input() responsive = true;
readonly type = input.required<string>();
readonly data = input<any>({});
readonly options = input<any>({});
readonly plugins = input<any[]>([]);
readonly width = input<string>();
readonly height = input<string>();
readonly responsive = input<boolean>(true);
@Output() dataSelect = new EventEmitter();
@Output() initialized = new EventEmitter<boolean>();
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
chart: any;
constructor() {
effect(() => {
const data = this.data();
const options = this.options();
untracked(() => {
if (this.chart) {
this.chart.destroy();
this.initChart();
}
});
});
}
ngAfterViewInit() {
import('chart.js/auto').then(module => {
Chart = module.default;
@ -91,19 +98,19 @@ export class ChartComponent implements AfterViewInit, OnDestroy, OnChanges {
}
private initChart = () => {
const opts = this.options || {};
opts.responsive = this.responsive;
const opts = this.options() || {};
opts.responsive = this.responsive();
// allows chart to resize in responsive mode
if (opts.responsive && (this.height || this.width)) {
if (opts.responsive && (this.height() || this.width())) {
opts.maintainAspectRatio = false;
}
this.chart = new Chart(this.canvas.nativeElement, {
type: this.type as any,
data: this.data,
options: this.options,
plugins: this.plugins,
type: this.type() as any,
data: this.data(),
options: this.options(),
plugins: this.plugins(),
});
};
@ -140,13 +147,5 @@ export class ChartComponent implements AfterViewInit, OnDestroy, OnChanges {
this.chart = null;
}
}
ngOnChanges(changes: SimpleChanges) {
if (!this.chart) return;
if (changes.data?.currentValue || changes.options?.currentValue) {
this.chart.destroy();
this.initChart();
}
}
}

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

@ -1,4 +1,4 @@
import { Directive, Injector, Input, inject } from '@angular/core';
import { Directive, Injector, inject, input } from '@angular/core';
import { ActionData, ActionList, InferredAction } from '../../models/actions';
import { ExtensionsService } from '../../services/extensions.service';
import { EXTENSIONS_ACTION_TYPE, EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token';
@ -14,7 +14,7 @@ export abstract class AbstractActionsComponent<
readonly getInjected: InferredData<L>['getInjected'];
@Input() record!: InferredData<L>['record'];
record = input.required<InferredRecord<L>>();
protected constructor() {
const injector = inject(Injector);
@ -27,3 +27,4 @@ export abstract class AbstractActionsComponent<
this.actionList = extensions[type].get(name).actions as unknown as L;
}
}

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">
@switch (getComponent(prop)) {
<ng-container *abpPermission="prop().permission; runChangeDetection: false">
@switch (getComponent(prop())) {
@case ('template') {
<ng-container *ngComponentOutlet="prop.template; injector: injectorForCustomComponent" />
<ng-container *ngComponentOutlet="prop().template; injector: injectorForCustomComponent" />
}
}
<div [class]="containerClassName" class="mb-2">
@switch (getComponent(prop)) {
@switch (getComponent(prop())) {
@case ('input') {
<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" />
}
@case ('hidden') {
<input [formControlName]="prop.name" type="hidden" />
<input [formControlName]="prop().name" type="hidden" />
}
@case ('checkbox') {
<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" />
<ng-template [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: 'form-check-label' }" />
</div>
}
@case ('select') {
<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">
@for (option of options$ | async; track option.value) {
<option [ngValue]="option.value">
@if (prop.isExtra) {
@if (prop().isExtra) {
{{ '::' + option.key | abpLocalization }}
} @else {
{{ option.key }}
@ -39,42 +39,42 @@
}
@case ('multiselect') {
<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" />
}
@case ('typeahead') {
<ng-template [ngTemplateOutlet]="label" />
<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"
[ngModelOptions]="{ standalone: true }" [(ngModel)]="typeaheadModel"
(selectItem)="setTypeaheadValue($event.item)" (blur)="setTypeaheadValue(typeaheadModel)"
[class.is-invalid]="typeahead.classList.contains('is-invalid')" class="form-control" />
<input [formControlName]="prop.name" type="hidden" />
<input [formControlName]="prop().name" type="hidden" />
</div>
}
@case ('date') {
<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" />
}
@case ('time') {
<ng-template [ngTemplateOutlet]="label" />
<ngb-timepicker [formControlName]="prop.name" />
<ngb-timepicker [formControlName]="prop().name" />
}
@case ('dateTime') {
<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') {
<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>
}
@case ('passwordinputgroup') {
<ng-template [ngTemplateOutlet]="label" />
<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">
<i class="fa" aria-hidden="true" [class]="{
'fa-eye-slash': !showPassword,
@ -85,27 +85,27 @@
}
}
@if (prop.formText) {
<small class="text-muted d-block">{{ prop.formText | abpLocalization }}</small>
@if (prop().formText) {
<small class="text-muted d-block">{{ prop().formText | abpLocalization }}</small>
}
</div>
</ng-container>
<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">
@if (prop.displayTextResolver) {
{{ prop.displayTextResolver(data) | abpLocalization }}
@if (prop().displayTextResolver) {
{{ prop().displayTextResolver(data()) | abpLocalization }}
} @else {
@if (prop.isExtra) {
{{ '::' + prop.displayName | abpLocalization }}
@if (prop().isExtra) {
{{ '::' + prop().displayName | abpLocalization }}
} @else {
{{ prop.displayName | abpLocalization }}
{{ prop().displayName | abpLocalization }}
}
}
{{ asterisk }}
@if (prop.tooltip) {
<i [ngbTooltip]="prop.tooltip.text | abpLocalization" [placement]="prop.tooltip.placement || 'auto'"
@if (prop().tooltip) {
<i [ngbTooltip]="prop().tooltip.text | abpLocalization" [placement]="prop().tooltip.placement || 'auto'"
container="body" class="bi bi-info-circle"></i>
}
</span>

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

@ -14,14 +14,11 @@ import {
ElementRef,
inject,
Injector,
Input,
OnChanges,
Optional,
SimpleChanges,
SkipSelf,
ViewChild,
signal,
effect,
input,
} from '@angular/core';
import {
ControlContainer,
@ -42,7 +39,7 @@ import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { DateAdapter, DisabledDirective, TimeAdapter } from '@abp/ng.theme.shared';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props';
import { PropData, ReadonlyPropData } from '../../models/props';
import { selfFactory } from '../../utils/factory.util';
import { addTypeaheadTextSuffix } from '../../utils/typeahead.util';
import { eExtensibleComponents } from '../../enums/components';
@ -72,8 +69,8 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
AsyncPipe,
NgComponentOutlet,
NgTemplateOutlet,
FormsModule
],
FormsModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ExtensibleFormPropService],
viewProviders: [
@ -86,7 +83,7 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
{ provide: NgbTimeAdapter, useClass: TimeAdapter },
],
})
export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
export class ExtensibleFormPropComponent implements AfterViewInit {
protected service = inject(ExtensibleFormPropService);
public readonly cdRef = inject(ChangeDetectorRef);
public readonly track = inject(TrackByService);
@ -94,10 +91,10 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
private injector = inject(Injector);
private readonly form = this.#groupDirective.form;
@Input() data!: PropData;
@Input() prop!: FormProp;
@Input() first?: boolean;
@Input() isFirstGroup?: boolean;
readonly data = input.required<PropData>();
readonly prop = input.required<FormProp>();
readonly first = input<boolean | undefined>(undefined);
readonly isFirstGroup = input<boolean | undefined>(undefined);
@ViewChild('field') private fieldRef!: ElementRef<HTMLElement>;
injectorForCustomComponent?: Injector;
@ -110,10 +107,55 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
typeaheadModel: any;
passwordKey = eExtensibleComponents.PasswordComponent;
disabledFn = (data: PropData) => false;
disabledFn = (data: ReadonlyPropData) => false;
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>) {
@ -130,7 +172,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
? text$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(text => this.prop?.options?.(this.data, text) || of([])),
switchMap(text => this.prop()?.options?.(this.data().data, text) || of([])),
)
: of([]);
@ -139,12 +181,12 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
meridian$ = this.service.meridian$;
get isInvalid() {
const control = this.form.get(this.prop.name);
const control = this.form.get(this.prop().name);
return control?.touched && control.invalid;
}
private getTypeaheadControls() {
const { name } = this.prop;
const { name } = this.prop();
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`;
const keyControl =
this.form.get(addTypeaheadTextSuffix(extraPropName)) ||
@ -158,7 +200,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
}
ngAfterViewInit() {
if (this.isFirstGroup && this.first && this.fieldRef) {
if (this.isFirstGroup() && this.first() && this.fieldRef) {
requestAnimationFrame(() => {
this.fieldRef.nativeElement.focus();
});
@ -172,43 +214,4 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
getType(prop: FormProp): string {
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) {
@for (groupedProp of groupedPropList.items; track i; let i = $index; let first = $first) {
<ng-container *abpPropData="let data; fromList: groupedProp.formPropList; withRecord: record">
@for (groupedProp of groupedPropList()?.items; track i; let i = $index; let first = $first) {
<ng-container *abpPropData="let data; fromList: groupedProp.formPropList; withRecord: record()">
@if (isAnyGroupMemberVisible(i, data) && groupedProp.group?.className) {
<div [class]="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,
Component,
inject,
Input,
Optional,
QueryList,
SkipSelf,
ViewChildren,
input,
signal,
effect
} from '@angular/core';
import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
@ -44,18 +46,22 @@ export class ExtensibleFormComponent<R = any> {
@ViewChildren(ExtensibleFormPropComponent)
formProps!: QueryList<ExtensibleFormPropComponent>;
@Input()
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;
}
readonly selectedRecord = input<R | undefined>(undefined);
extraPropertiesKey = EXTRA_PROPERTIES_KEY;
groupedPropList!: GroupedFormPropList;
readonly groupedPropList = signal<GroupedFormPropList | undefined>(undefined);
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 {
return (this.container ? this.container.control : { controls: {} }) as UntypedFormGroup;
@ -75,8 +81,10 @@ export class ExtensibleFormComponent<R = any> {
}
//TODO: Reactor this method
isAnyGroupMemberVisible(index: number, data) {
const { items } = this.groupedPropList;
isAnyGroupMemberVisible(index: number, data: any) {
const groupedPropListValue = this.groupedPropList();
if (!groupedPropListValue) return false;
const { items } = groupedPropListValue;
const formPropList = items[index].formPropList.toArray();
return formPropList.some(prop => prop.visible(data));
}

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

@ -1,119 +1,202 @@
@if (isBrowser) {
<ngx-datatable #table default [rows]="data" [count]="recordsTotal" [list]="list"
[selectionType]="selectable ? _selectionType : undefined" (activate)="tableActivate.emit($event)"
(select)="onSelect($event)" [selected]="selected" (scroll)="onScroll($event)" [scrollbarV]="infiniteScroll"
[style.height]="getTableHeight()" [loadingIndicator]="infiniteScroll && isLoading"
[footerHeight]="infiniteScroll ? false : 50">
@if (effectiveRowDetailTemplate) {
<ngx-datatable-row-detail [rowHeight]="effectiveRowDetailHeight">
<ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
<ng-container
*ngTemplateOutlet="effectiveRowDetailTemplate; context: { row: row, expanded: expanded }" />
</ng-template>
</ngx-datatable-row-detail>
<ngx-datatable
#table
default
[rows]="data"
[count]="recordsTotal()"
[list]="list()"
[selectionType]="selectable() ? selectionType() : undefined"
(activate)="tableActivate.emit($event)"
(select)="onSelect($event)"
[selected]="selected()"
(scroll)="onScroll($event)"
[scrollbarV]="infiniteScroll()"
[style.height]="getTableHeight()"
[loadingIndicator]="infiniteScroll() && isLoading()"
[footerHeight]="infiniteScroll() ? false : 50"
>
@if (effectiveRowDetailTemplate) {
<ngx-datatable-row-detail [rowHeight]="effectiveRowDetailHeight">
<ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
<ng-container
*ngTemplateOutlet="
effectiveRowDetailTemplate;
context: { row: row, expanded: expanded }
"
/>
</ng-template>
</ngx-datatable-row-detail>
<ngx-datatable-column [width]="50" [resizeable]="false" [sortable]="false" [draggable]="false"
[canAutoResize]="false">
<ng-template let-row="row" let-expanded="expanded" ngx-datatable-cell-template>
<button type="button" class="btn btn-link text-decoration-none text-muted p-0"
[attr.aria-label]="expanded ? 'Collapse' : 'Expand'" (click)="toggleExpandRow(row)">
<i class="fa" [class.fa-chevron-down]="!expanded" [class.fa-chevron-up]="expanded"></i>
</button>
</ng-template>
</ngx-datatable-column>
}
@if(selectable) {
<ngx-datatable-column [width]="50" [sortable]="false" [canAutoResize]="false" [draggable]="false"
[resizeable]="false">
<ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn">
@if (_selectionType !== 'single') {
<div class="form-check">
<input class="form-check-input table-check" type="checkbox" [checked]="allRowsSelected"
(change)="selectFn(!allRowsSelected)" />
</div>
}
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value" let-row="row" let-isSelected="isSelected"
let-onCheckboxChangeFn="onCheckboxChangeFn">
@if(_selectionType === 'single') {
<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)" />
</div>
}
@if (_selectionType !== 'single') {
<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)" />
</div>
}
</ng-template>
<ngx-datatable-column
[width]="50"
[resizeable]="false"
[sortable]="false"
[draggable]="false"
[canAutoResize]="false"
>
<ng-template let-row="row" let-expanded="expanded" ngx-datatable-cell-template>
<button
type="button"
class="btn btn-link text-decoration-none text-muted p-0"
[attr.aria-label]="expanded ? 'Collapse' : 'Expand'"
(click)="toggleExpandRow(row)"
>
<i class="fa" [class.fa-chevron-down]="!expanded" [class.fa-chevron-up]="expanded"></i>
</button>
</ng-template>
</ngx-datatable-column>
}
@if (selectable()) {
<ngx-datatable-column
[width]="50"
[sortable]="false"
[canAutoResize]="false"
[draggable]="false"
[resizeable]="false"
>
<ng-template
ngx-datatable-header-template
let-value="value"
let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn"
>
@if (selectionType() !== 'single') {
<div class="form-check">
<input
class="form-check-input table-check"
type="checkbox"
[checked]="allRowsSelected"
(change)="selectFn(!allRowsSelected)"
/>
</div>
}
</ng-template>
</ngx-datatable-column>
}
@if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) {
<ngx-datatable-column [name]="actionsText | abpLocalization" [maxWidth]="_actionsColumnWidth() ?? undefined"
[width]="_actionsColumnWidth() ?? 200" [canAutoResize]="!_actionsColumnWidth()" [sortable]="false">
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }"></ng-container>
<ng-template #gridActions>
@if (isVisibleActions(row)) {
<abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions>
}
</ng-template>
</ng-template>
</ngx-datatable-column>
}
@for (prop of propList; track prop.name; let i = $index) {
<ngx-datatable-column *abpVisible="prop.columnVisible(getInjected)" [width]="columnWidths[i] ?? 200"
[canAutoResize]="!columnWidths[i]"
[name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization" [prop]="prop.name"
[sortable]="prop.sortable">
<ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn">
@if (prop.tooltip) {
<span [ngbTooltip]="prop.tooltip.text | abpLocalization" [placement]="prop.tooltip.placement || 'auto'"
container="body" [class.pointer]="prop.sortable" (click)="prop.sortable && sortFn(column)">
{{ column.name }} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
} @else {
<span [class.pointer]="prop.sortable" (click)="prop.sortable && sortFn(column)">
{{ column.name }}
</span>
}
</ng-template>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *abpVisible="row['_' + prop.name]?.visible">
@if (!row['_' + prop.name].component) {
@if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') {
<div [innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type)
: ('::' + (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type) | abpLocalization)
" (click)="
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
" [class]="entityPropTypeClasses[prop.type]" [class.pointer]="prop.action"></div>
} @else {
<div [innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async)
: ('::' + (row['_' + prop.name]?.value | async) | abpLocalization)
" (click)="
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
" [class]="entityPropTypeClasses[prop.type]" [class.pointer]="prop.action"></div>
<ng-template
ngx-datatable-cell-template
let-value="value"
let-row="row"
let-isSelected="isSelected"
let-onCheckboxChangeFn="onCheckboxChangeFn"
>
@if (selectionType() === 'single') {
<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)"
/>
</div>
}
@if (selectionType() !== 'single') {
<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)"
/>
</div>
}
</ng-template>
</ngx-datatable-column>
}
@if (actionsTemplate() || (actionList.length && hasAtLeastOnePermittedAction)) {
<ngx-datatable-column
[name]="actionsText() | abpLocalization"
[maxWidth]="_actionsColumnWidth() ?? undefined"
[width]="_actionsColumnWidth() ?? 200"
[canAutoResize]="!_actionsColumnWidth()"
[sortable]="false"
>
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container
*ngTemplateOutlet="
actionsTemplate() || gridActions;
context: { $implicit: row, index: i }
"
></ng-container>
<ng-template #gridActions>
@if (isVisibleActions(row)) {
<abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions>
}
</ng-template>
</ng-template>
</ngx-datatable-column>
}
@for (prop of propList; track prop.name; let i = $index) {
<ngx-datatable-column
*abpVisible="prop.columnVisible(getInjected)"
[width]="columnWidths()[i] ?? 200"
[canAutoResize]="!columnWidths()[i]"
[name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization"
[prop]="prop.name"
[sortable]="prop.sortable"
>
<ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn">
@if (prop.tooltip) {
<span
[ngbTooltip]="prop.tooltip.text | abpLocalization"
[placement]="prop.tooltip.placement || 'auto'"
container="body"
[class.pointer]="prop.sortable"
(click)="prop.sortable && sortFn(column)"
>
{{ column.name }} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
} @else {
<ng-container *ngComponentOutlet="
row['_' + prop.name].component;
injector: row['_' + prop.name].injector
"></ng-container>
<span [class.pointer]="prop.sortable" (click)="prop.sortable && sortFn(column)">
{{ column.name }}
</span>
}
</ng-container>
</ng-container>
</ng-template>
</ngx-datatable-column>
}
</ngx-datatable>
}
</ng-template>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *abpVisible="row['_' + prop.name]?.visible">
@if (!row['_' + prop.name].component) {
@if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') {
<div
[innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async | abpUtcToLocal: prop.type)
: ('::' + (row['_' + prop.name]?.value | async | abpUtcToLocal: prop.type)
| abpLocalization)
"
(click)="
prop.action &&
prop.action({ getInjected: getInjected, record: row, index: i })
"
[class]="entityPropTypeClasses[prop.type]"
[class.pointer]="prop.action"
></div>
} @else {
<div
[innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async)
: ('::' + (row['_' + prop.name]?.value | async) | abpLocalization)
"
(click)="
prop.action &&
prop.action({ getInjected: getInjected, record: row, index: i })
"
[class]="entityPropTypeClasses[prop.type]"
[class.pointer]="prop.action"
></div>
}
} @else {
<ng-container
*ngComponentOutlet="
row['_' + prop.name].component;
injector: row['_' + prop.name].injector
"
></ng-container>
}
</ng-container>
</ng-container>
</ng-template>
</ngx-datatable-column>
}
</ngx-datatable>
}

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

@ -5,20 +5,18 @@ import {
Component,
computed,
ContentChild,
EventEmitter,
inject,
Injector,
Input,
LOCALE_ID,
OnChanges,
OnDestroy,
Output,
PLATFORM_ID,
signal,
SimpleChanges,
TemplateRef,
TrackByFunction,
ViewChild,
input,
effect,
output,
} from '@angular/core';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
@ -46,7 +44,7 @@ import {
import { ePropType } from '../../enums/props.enum';
import { EntityActionList } from '../../models/entity-actions';
import { EntityProp, EntityPropList } from '../../models/entity-props';
import { PropData } from '../../models/props';
import { ReadonlyPropData } from '../../models/props';
import { ExtensionsService } from '../../services/extensions.service';
import {
ENTITY_PROP_TYPE_CLASSES,
@ -79,14 +77,16 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
],
templateUrl: './extensible-table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [`
:host ::ng-deep .ngx-datatable.material .datatable-body .datatable-row-detail {
background: none;
padding: 0;
}
`],
styles: [
`
:host ::ng-deep .ngx-datatable.material .datatable-body .datatable-row-detail {
background: none;
padding: 0;
}
`,
],
})
export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewInit, OnDestroy {
export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestroy {
readonly #injector = inject(Injector);
readonly getInjected = this.#injector.get.bind(this.#injector);
protected readonly cdr = inject(ChangeDetectorRef);
@ -98,60 +98,66 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
private platformId = inject(PLATFORM_ID);
protected isBrowser = isPlatformBrowser(this.platformId);
protected _actionsText!: string;
@Input()
set actionsText(value: string) {
this._actionsText = value;
}
get actionsText(): string {
return this._actionsText ?? (this.actionList.length >= 1 ? 'AbpUi::Actions' : '');
}
@Input() data!: R[];
@Input() list!: ListService;
@Input() recordsTotal!: number;
// Input signals
readonly actionsTextInput = input<string | undefined>(undefined, { alias: 'actionsText' });
readonly dataInput = input<R[]>([], { alias: 'data' });
readonly list = input.required<ListService>();
readonly recordsTotal = input.required<number>();
readonly actionsColumnWidthInput = input<number | undefined>(undefined, {
alias: 'actionsColumnWidth',
});
readonly actionsTemplate = input<TemplateRef<any> | undefined>(undefined);
readonly selectable = input(false);
readonly selectionTypeInput = input<SelectionType | string>(SelectionType.multiClick, {
alias: 'selectionType',
});
readonly selected = input<any[]>([]);
readonly infiniteScroll = input(false);
readonly isLoading = input(false);
readonly scrollThreshold = input(10);
readonly tableHeight = input<number | undefined>(undefined);
readonly rowDetailTemplate = input<TemplateRef<RowDetailContext<R>> | undefined>(undefined);
readonly rowDetailHeight = input<string | number>('100%');
// 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);
@Input() set actionsColumnWidth(width: number) {
this._actionsColumnWidth.set(width ? Number(width) : undefined);
}
@ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@Input() actionsTemplate?: TemplateRef<any>;
@ViewChild('table', { static: false }) table!: DatatableComponent;
@Output() tableActivate = new EventEmitter();
// Computed values
protected readonly actionsText = computed(() => {
return this.actionsTextInput() ?? (this.actionList.length >= 1 ? 'AbpUi::Actions' : '');
});
@Input() selectable = false;
protected readonly selectionType = computed(() => {
const value = this.selectionTypeInput();
return typeof value === 'string' ? SelectionType[value as keyof typeof SelectionType] : value;
});
@Input() set selectionType(value: SelectionType | string) {
this._selectionType = typeof value === 'string' ? SelectionType[value] : value;
protected get data(): R[] {
return this._data();
}
_selectionType: SelectionType = SelectionType.multiClick;
@Input() selected: any[] = [];
@Output() selectionChange = new EventEmitter<any[]>();
// Infinite scroll configuration
@Input() infiniteScroll = false;
@Input() isLoading = false;
@Input() scrollThreshold = 10;
@Output() loadMore = new EventEmitter<void>();
@Input() tableHeight: number;
@Input() rowDetailTemplate?: TemplateRef<RowDetailContext<R>>;
@Input() rowDetailHeight: string | number = '100%';
@Output() rowDetailToggle = new EventEmitter<R>();
@ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@ViewChild('table', { static: false }) table!: DatatableComponent;
protected set data(value: R[]) {
this._data.set(value);
}
protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate;
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate();
}
protected get effectiveRowDetailHeight(): string | number {
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight;
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight();
}
hasAtLeastOnePermittedAction: boolean;
@ -162,9 +168,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
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
private readonly loadMoreSubject = new Subject<void>();
private readonly loadMoreSubscription = this.loadMoreSubject
@ -186,6 +189,55 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
this.permissionService.filterItemsByPolicy(
this.actionList.toArray().map(action => ({ requiredPolicy: action.permission })),
).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) {
@ -200,7 +252,7 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return key;
}
getContent(prop: EntityProp<R>, data: PropData): Observable<string> {
getContent(prop: EntityProp<R>, data: ReadonlyPropData): Observable<string> {
return prop.valueResolver(data).pipe(
map(value => {
switch (prop.type) {
@ -216,45 +268,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 {
const actions = this.actionList.toArray();
const visibleActions = actions.filter(action => {
@ -277,9 +290,10 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return visibleActions.length > 0;
}
onSelect({ selected }) {
this.selected.splice(0, this.selected.length);
this.selected.push(...selected);
onSelect({ selected }: { selected: any[] }) {
const selectedValue = this.selected();
selectedValue.splice(0, selectedValue.length);
selectedValue.push(...selected);
this.selectionChange.emit(selected);
}
@ -299,12 +313,12 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
}
private shouldHandleScroll(): boolean {
return this.infiniteScroll && !this.isLoading;
return this.infiniteScroll() && !this.isLoading();
}
private isNearScrollBottom(element: HTMLElement): boolean {
const { offsetHeight, scrollTop, scrollHeight } = element;
return offsetHeight + scrollTop >= scrollHeight - this.scrollThreshold;
return offsetHeight + scrollTop >= scrollHeight - this.scrollThreshold();
}
private triggerLoadMore(): void {
@ -312,9 +326,10 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
}
getTableHeight() {
if (!this.infiniteScroll) return 'auto';
if (!this.infiniteScroll()) return 'auto';
return this.tableHeight ? `${this.tableHeight}px` : 'auto';
const tableHeight = this.tableHeight();
return tableHeight ? `${tableHeight}px` : 'auto';
}
toggleExpandRow(row: R): void {
@ -325,11 +340,13 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
}
ngAfterViewInit(): void {
if (!this.infiniteScroll) {
this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => {
this.data = [];
this.cdr.markForCheck();
});
if (!this.infiniteScroll()) {
this.list()
?.requestStatus$?.pipe(filter(status => status === 'loading'))
.subscribe(() => {
this._data.set([]);
this.cdr.markForCheck();
});
}
}

2
npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html

@ -1,7 +1,7 @@
@if (actionList.length > 1) {
<div ngbDropdown container="body" class="d-inline-block">
<button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" ngbDropdownToggle>
<i [class]="icon" [class.me-1]="icon"></i>{{ text | abpLocalization }}
<i [class]="icon()" [class.me-1]="icon()"></i>{{ text() | abpLocalization }}
</button>
<div ngbDropdownMenu>
@for (action of actionList; track action.text) {

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

@ -1,8 +1,8 @@
import {
ChangeDetectionStrategy,
Component,
Input,
TrackByFunction,
input
} from '@angular/core';
import { EntityAction, EntityActionList } from '../../models/entity-actions';
import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token';
@ -33,11 +33,9 @@ import { NgTemplateOutlet } from '@angular/common';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GridActionsComponent<R = any> extends AbstractActionsComponent<EntityActionList<R>> {
@Input() icon = 'fa fa-cog';
@Input() readonly index?: number;
@Input() text = '';
readonly icon = input('fa fa-cog');
readonly index = input<number | undefined>(undefined);
readonly text = input('');
readonly trackByFn: TrackByFunction<EntityAction<R>> = (_, item) => item.text;

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

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

25
npm/ng-packs/packages/components/extensible/src/lib/models/actions.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';
export abstract class ActionList<R = any, A = Action<R>> extends LinkedList<A> {}
@ -9,19 +9,28 @@ export abstract class ActionData<R = any> {
notFoundValue?: T,
flags?: InjectOptions,
) => T;
index?: number;
abstract record: R;
index?: number | InputSignal<number | undefined>;
abstract record: R | InputSignal<R>;
get data(): ReadonlyActionData<R> {
return {
getInjected: this.getInjected,
index: this.index,
record: this.record,
// `record` / `index` may be signals; always use `data.record` / `data.index`.
index: isSignal(this.index) ? this.index() : this.index,
record: isSignal(this.record) ? this.record() : this.record,
};
}
}
export type ReadonlyActionData<R = any> = Readonly<Omit<ActionData<R>, 'data'>>;
export type ReadonlyActionData<R = any> = Readonly<{
getInjected: <T>(
token: Type<T> | InjectionToken<T>,
notFoundValue?: T,
flags?: InjectOptions,
) => T;
index?: number;
record: R;
}>;
export abstract class Action<R = any> {
constructor(
@ -33,8 +42,8 @@ export abstract class Action<R = any> {
) {}
}
export type ActionCallback<T, R = any> = (data: Omit<ActionData<T>, 'data'>) => R;
export type ActionPredicate<T> = (data?: Omit<ActionData<T>, 'data'>) => boolean;
export type ActionCallback<T, R = any> = (data: ReadonlyActionData<T>) => R;
export type ActionPredicate<T> = (data?: ReadonlyActionData<T>) => boolean;
export abstract class ActionsFactory<C extends Actions<any>> {
protected abstract _ctor: Type<C>;

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 { ePropType } from '../enums/props.enum';
import { FormPropTooltip } from './form-props';
@ -11,19 +11,28 @@ export abstract class PropData<R = any> {
notFoundValue?: T,
options?: InjectOptions,
) => T;
index?: number;
abstract record: R;
index?: number | InputSignal<number | undefined>;
abstract record: R | InputSignal<R>;
get data(): ReadonlyPropData<R> {
return {
getInjected: this.getInjected,
index: this.index,
record: this.record,
// `record` / `index` may be signals; always use `data.record` / `data.index`.
index: isSignal(this.index) ? this.index() : this.index,
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> {
constructor(
@ -43,9 +52,9 @@ export abstract class Prop<R = any> {
}
}
export type PropCallback<T, R = any> = (data: Omit<PropData<T>, 'data'>, auxData?: any) => R;
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>, auxData?: any) => boolean;
export type PropDisplayTextResolver<T> = (data?: Omit<PropData<T>, 'data'>) => string;
export type PropCallback<T, R = any> = (data: ReadonlyPropData<T>, auxData?: any) => R;
export type PropPredicate<T> = (data?: ReadonlyPropData<T>, auxData?: any) => boolean;
export type PropDisplayTextResolver<T> = (data?: ReadonlyPropData<T>) => string;
export abstract class PropsFactory<C extends Props<any>> {
protected abstract _ctor: Type<C>;

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

@ -4,11 +4,13 @@ import { DateTimeAdapter, DateAdapter, TimeAdapter } from '@abp/ng.theme.shared'
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties';
import { ePropType } from '../enums/props.enum';
import { FormPropList } from '../models/form-props';
import { PropData } from '../models/props';
import { PropData, ReadonlyPropData } from '../models/props';
import { ExtensionsService } from '../services/extensions.service';
import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token';
export function generateFormFromProps<R = any>(data: PropData<R>) {
export function generateFormFromProps<R = any>(propData: PropData<R>) {
const data: ReadonlyPropData<R> = propData.data;
const extensions = data.getInjected(ExtensionsService<R>);
const identifier = data.getInjected(EXTENSIONS_IDENTIFIER);
@ -16,7 +18,7 @@ export function generateFormFromProps<R = any>(data: PropData<R>) {
const extraForm = new UntypedFormGroup({});
form.addControl(EXTRA_PROPERTIES_KEY, extraForm);
const record = data.record || {};
const record: any = data.record || {};
const type = JSON.stringify(record) === '{}' ? 'create' : 'edit';
const props: FormPropList<R> = extensions[`${type}FormProps`].get(identifier).props;
const extraProperties = record[EXTRA_PROPERTIES_KEY] || {};
@ -33,7 +35,7 @@ export function generateFormFromProps<R = any>(data: PropData<R>) {
value = record[name];
}
if (typeof value === 'undefined') value = prop.defaultValue;
if (typeof value === 'undefined') value = prop.defaultValue as any;
if (value) {
let adapter: DateAdapter | TimeAdapter | DateTimeAdapter;

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

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

12
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>
}
@ -15,7 +15,7 @@
@if (customBreadcrumb) {
<ng-content select="abp-page-breadcrumb-container"></ng-content>
} @else {
@if (breadcrumb) {
@if (breadcrumb()) {
<div class="col-lg-auto ps-lg-0" *abpPagePart="pageParts.breadcrumb">
<abp-breadcrumb></abp-breadcrumb>
</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>
}
}

36
npm/ng-packs/packages/components/page/src/page.component.ts

@ -1,4 +1,4 @@
import { Component, Input, ViewEncapsulation, ContentChild } from '@angular/core';
import { Component, ViewEncapsulation, ContentChild, input, effect, signal } from '@angular/core';
import {
PageTitleContainerComponent,
PageBreadcrumbContainerComponent,
@ -16,20 +16,12 @@ import { PagePartDirective } from './page-part.directive';
imports: [BreadcrumbComponent, PageToolbarComponent, PagePartDirective],
})
export class PageComponent {
@Input() title?: string;
readonly title = input<string | undefined>(undefined);
readonly toolbarInput = input<any>(undefined, { alias: 'toolbar' });
readonly breadcrumb = input(true);
toolbarVisible = false;
_toolbarData: any;
@Input() set toolbar(val: any) {
this._toolbarData = val;
this.toolbarVisible = true;
}
get toolbarData() {
return this._toolbarData;
}
@Input() breadcrumb = true;
protected readonly toolbarVisible = signal(false);
protected readonly toolbarData = signal<any>(undefined);
pageParts = {
title: PageParts.title,
@ -42,11 +34,21 @@ 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.breadcrumb ||
this.title() ||
this.toolbarVisible() ||
this.breadcrumb() ||
this.customTitle ||
this.customBreadcrumb ||
this.customToolbar ||

8
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html

@ -1,8 +1,8 @@
<nz-tree
[nzBeforeDrop]="beforeDrop"
[nzDraggable]="draggable"
[nzCheckStrictly]="checkStrictly"
[nzCheckable]="checkable"
[nzDraggable]="draggable()"
[nzCheckStrictly]="checkStrictly()"
[nzCheckable]="checkable()"
[nzCheckedKeys]="checkedKeys"
[nzData]="nodes"
[nzTreeTemplate]="treeTemplate"
@ -11,7 +11,7 @@
(nzExpandChange)="onExpandedKeysChange($event)"
(nzCheckboxChange)="onCheckboxChange($event)"
(nzOnDrop)="onDrop($event)"
[nzNoAnimation]="noAnimation"
[nzNoAnimation]="noAnimation()"
(nzContextMenu)="onContextMenuChange($event)"
/>
<ng-template #treeTemplate let-node>

121
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<any>;
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective;
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective;
@Output() readonly checkedKeysChange = new EventEmitter();
@Output() readonly expandedKeysChange = new EventEmitter<string[]>();
@Output() readonly selectedNodeChange = new EventEmitter();
@Output() readonly dropOver = new EventEmitter<DropEvent>();
@Output() readonly nzExpandChange = new EventEmitter<NzFormatEmitEvent>();
@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<any>;
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate!: TreeNodeTemplateDirective;
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate!: ExpandedIconTemplateDirective;
// Output signals
readonly checkedKeysChange = output<any>();
readonly expandedKeysChange = output<string[]>();
readonly selectedNodeChange = output<any>();
readonly dropOver = output<DropEvent>();
readonly nzExpandChange = output<NzFormatEmitEvent>();
// Input signals
readonly noAnimation = input(true);
readonly draggable = input<boolean | undefined>(undefined);
readonly checkable = input<boolean | undefined>(undefined);
readonly checkStrictly = input<boolean | undefined>(undefined);
readonly checkedKeysInput = input<any[]>([], { alias: 'checkedKeys' });
readonly nodesInput = input<any[]>([], { alias: 'nodes' });
readonly expandedKeysInput = input<string[]>([], { alias: 'expandedKeys' });
readonly selectedNodeInput = input<any>(undefined, { alias: 'selectedNode' });
readonly changeCheckboxWithNode = input<boolean | undefined>(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<any[]>([]);
protected readonly _expandedKeys = signal<string[]>([]);
protected readonly _selectedNode = signal<any>(undefined);
protected readonly _nodes = signal<any[]>([]);
// 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();
}
}

21
npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, inject, 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,23 +11,20 @@ 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;
@Input()
readonly?: boolean;
readonly readonly = input<boolean | undefined>(undefined);
@Input()
valueFn: (value: U, previousValue?: T) => T = value => value as any as T;
readonly valueFn = input<(value: U, previousValue?: T) => T>(value => value as any as T);
@Input()
valueLimitFn: (value: T, previousValue?: T) => any = value => false;
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);
value = this.valueFn()(value as any as U, this._value);
if (this.valueLimitFn(value, this._value) !== false || this.readonly) return;
if (this.valueLimitFn()(value, this._value) !== false || this.readonly()) return;
this._value = value;
this.notifyValueChange();
@ -48,7 +45,7 @@ export class AbstractNgModelComponent<T = any, U = T> implements ControlValueAcc
}
writeValue(value: T): void {
this._value = this.valueLimitFn(value, this._value) || value;
this._value = this.valueLimitFn()(value, this._value) || value;
this.cdRef.markForCheck();
}

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

6
npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts

@ -1,4 +1,4 @@
import { Directive, ElementRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
import { Directive, ElementRef, EventEmitter, OnInit, Output, inject, input } from '@angular/core';
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { SubscriptionService } from '../services/subscription.service';
@ -11,13 +11,13 @@ export class InputEventDebounceDirective implements OnInit {
private el = inject(ElementRef);
private subscription = inject(SubscriptionService);
@Input() debounce = 300;
readonly debounce = input(300);
@Output('input.debounce') readonly debounceEvent = new EventEmitter<Event>();
ngOnInit(): void {
const input$ = fromEvent<InputEvent>(this.el.nativeElement, 'input').pipe(
debounceTime(this.debounce),
debounceTime(this.debounce()),
);
this.subscription.addOne(input$, (event: Event) => {

63
npm/ng-packs/packages/core/src/lib/directives/for.directive.ts

@ -1,7 +1,6 @@
import {
Directive,
EmbeddedViewRef,
Input,
IterableChangeRecord,
IterableChanges,
IterableDiffer,
@ -11,6 +10,7 @@ import {
TrackByFunction,
ViewContainerRef,
inject,
input
} from '@angular/core';
import clone from 'just-clone';
import compare from 'just-compare';
@ -42,29 +42,21 @@ export class ForDirective implements OnChanges {
private differs = inject(IterableDiffers);
// eslint-disable-next-line @angular-eslint/no-input-rename
@Input('abpForOf')
items!: any[];
readonly items = input.required<any[]>({ alias: "abpForOf" });
@Input('abpForOrderBy')
orderBy?: string;
readonly orderBy = input<string>(undefined, { alias: "abpForOrderBy" });
@Input('abpForOrderDir')
orderDir?: 'ASC' | 'DESC';
readonly orderDir = input<'ASC' | 'DESC'>(undefined, { alias: "abpForOrderDir" });
@Input('abpForFilterBy')
filterBy?: string;
readonly filterBy = input<string>(undefined, { alias: "abpForFilterBy" });
@Input('abpForFilterVal')
filterVal: any;
readonly filterVal = input<any>(undefined, { alias: "abpForFilterVal" });
@Input('abpForTrackBy')
trackBy?: TrackByFunction<any>;
readonly trackBy = input<TrackByFunction<any>>(undefined, { alias: "abpForTrackBy" });
@Input('abpForCompareBy')
compareBy?: CompareFn;
readonly compareBy = input<CompareFn>(undefined, { alias: "abpForCompareBy" });
@Input('abpForEmptyRef')
emptyRef?: TemplateRef<any>;
readonly emptyRef = input<TemplateRef<any>>(undefined, { alias: "abpForEmptyRef" });
private differ!: IterableDiffer<any> | null;
private lastItemsRef: any[] | null = null;
@ -72,11 +64,11 @@ export class ForDirective implements OnChanges {
private isShowEmptyRef!: boolean;
get compareFn(): CompareFn {
return this.compareBy || compare;
return this.compareBy() || compare;
}
get trackByFn(): TrackByFunction<any> {
return this.trackBy || ((index: number, item: any) => (item as any).id || index);
return this.trackBy() || ((index: number, item: any) => (item as any).id || index);
}
private iterateOverAppliedOperations(changes: IterableChanges<any>) {
@ -91,7 +83,7 @@ export class ForDirective implements OnChanges {
if (record.previousIndex == null) {
const view = this.vcRef.createEmbeddedView(
this.tempRef,
new AbpForContext(null, -1, -1, this.items),
new AbpForContext(null, -1, -1, this.items()),
currentIndex || 0,
);
@ -120,7 +112,7 @@ export class ForDirective implements OnChanges {
const viewRef = this.vcRef.get(i) as EmbeddedViewRef<AbpForContext>;
viewRef.context.index = i;
viewRef.context.count = l;
viewRef.context.list = this.items;
viewRef.context.list = this.items();
}
changes.forEachIdentityChange((record: IterableChangeRecord<any>) => {
@ -132,9 +124,10 @@ export class ForDirective implements OnChanges {
}
private projectItems(items: any[]): void {
if (!items.length && this.emptyRef) {
const emptyRef = this.emptyRef();
if (!items.length && emptyRef) {
this.vcRef.clear();
this.vcRef.createEmbeddedView(this.emptyRef).rootNodes;
this.vcRef.createEmbeddedView(emptyRef).rootNodes;
this.isShowEmptyRef = true;
this.differ = null;
this.lastItemsRef = null;
@ -142,7 +135,7 @@ export class ForDirective implements OnChanges {
return;
}
if (this.emptyRef && this.isShowEmptyRef) {
if (emptyRef && this.isShowEmptyRef) {
this.vcRef.clear();
this.isShowEmptyRef = false;
}
@ -162,7 +155,7 @@ export class ForDirective implements OnChanges {
}
private sortItems(items: any[]) {
const orderBy = this.orderBy;
const orderBy = this.orderBy();
if (orderBy) {
items.sort((a, b) => (a[orderBy] > b[orderBy] ? 1 : a[orderBy] < b[orderBy] ? -1 : 0));
} else {
@ -171,28 +164,30 @@ export class ForDirective implements OnChanges {
}
ngOnChanges() {
if (!this.items) return;
const itemsValue = this.items();
if (!itemsValue) return;
// Recreate differ if items array reference changed
if (this.lastItemsRef !== this.items) {
if (this.lastItemsRef !== itemsValue) {
this.differ = null;
this.lastItemsRef = this.items;
this.lastItemsRef = itemsValue;
}
let items = clone(this.items) as any[];
let items = clone(itemsValue) as any[];
if (!Array.isArray(items)) return;
const compareFn = this.compareFn;
const filterBy = this.filterBy;
const filterBy = this.filterBy();
const filterVal = this.filterVal();
if (
typeof filterBy !== 'undefined' &&
typeof this.filterVal !== 'undefined' &&
this.filterVal !== ''
typeof filterVal !== 'undefined' &&
filterVal !== ''
) {
items = items.filter(item => compareFn(item[filterBy], this.filterVal));
items = items.filter(item => compareFn(item[filterBy], this.filterVal()));
}
switch (this.orderDir) {
switch (this.orderDir()) {
case 'ASC':
this.sortItems(items);
this.projectItems(items);

31
npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts

@ -1,12 +1,12 @@
import {
ChangeDetectorRef,
Directive,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
inject
import {
ChangeDetectorRef,
Directive,
ElementRef,
EventEmitter,
OnInit,
Output,
inject,
input
} from '@angular/core';
import { FormGroupDirective, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { fromEvent } from 'rxjs';
@ -27,15 +27,12 @@ export class FormSubmitDirective implements OnInit {
private cdRef = inject(ChangeDetectorRef);
private subscription = inject(SubscriptionService);
@Input()
debounce = 200;
readonly debounce = input(200);
// TODO: Remove unused input
@Input()
notValidateOnSubmit?: string | boolean;
readonly notValidateOnSubmit = input<string | boolean>(undefined);
@Input()
markAsDirtyWhenSubmit = true;
readonly markAsDirtyWhenSubmit = input(true);
@Output() readonly ngSubmit = new EventEmitter();
@ -43,7 +40,7 @@ export class FormSubmitDirective implements OnInit {
ngOnInit() {
this.subscription.addOne(this.formGroupDirective.ngSubmit, () => {
if (this.markAsDirtyWhenSubmit) {
if (this.markAsDirtyWhenSubmit()) {
this.markAsDirty();
}
@ -51,7 +48,7 @@ export class FormSubmitDirective implements OnInit {
});
const keyup$ = fromEvent<KeyboardEvent>(this.host.nativeElement as HTMLElement, 'keyup').pipe(
debounceTime(this.debounce),
debounceTime(this.debounce()),
filter(event => !(event.target instanceof HTMLTextAreaElement)),
filter(event => event && event.key === 'Enter'),
);

10
npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts

@ -2,12 +2,12 @@ import {
AfterViewInit,
ChangeDetectorRef,
Directive,
Input,
OnChanges,
OnDestroy,
TemplateRef,
ViewContainerRef,
inject,
input
} from '@angular/core';
import { ReplaySubject, Subscription } from 'rxjs';
import { distinctUntilChanged, take } from 'rxjs/operators';
@ -25,9 +25,9 @@ export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit
private cdRef = inject(ChangeDetectorRef);
queue = inject<QueueManager>(QUEUE_MANAGER);
@Input('abpPermission') condition: string | undefined;
readonly condition = input<string | undefined>(undefined, { alias: "abpPermission" });
@Input('abpPermissionRunChangeDetection') runChangeDetection = true;
readonly runChangeDetection = input(true, { alias: "abpPermissionRunChangeDetection" });
subscription!: Subscription;
@ -41,14 +41,14 @@ export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit
}
this.subscription = this.permissionService
.getGrantedPolicy$(this.condition || '')
.getGrantedPolicy$(this.condition() || '')
.pipe(distinctUntilChanged())
.subscribe(isGranted => {
this.vcRef.clear();
if (isGranted && this.templateRef) {
this.vcRef.createEmbeddedView(this.templateRef);
}
if (this.runChangeDetection) {
if (this.runChangeDetection()) {
if (!this.rendered) {
this.cdrSubject.next();
} else {

70
npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts

@ -1,14 +1,14 @@
import {
Directive,
Injector,
Input,
OnChanges,
OnInit,
SimpleChanges,
TemplateRef,
Type,
ViewContainerRef,
inject
import {
Directive,
Injector,
OnChanges,
OnInit,
SimpleChanges,
TemplateRef,
Type,
ViewContainerRef,
inject,
input
} from '@angular/core';
import compare from 'just-compare';
import { Subscription } from 'rxjs';
@ -29,8 +29,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
private replaceableComponents = inject(ReplaceableComponentsService);
private subscription = inject(SubscriptionService);
@Input('abpReplaceableTemplate')
data!: ReplaceableComponents.ReplaceableTemplateDirectiveInput<any, any>;
readonly data = input.required<ReplaceableComponents.ReplaceableTemplateDirectiveInput<any, any>>({ alias: "abpReplaceableTemplate" });
providedData = {
inputs: {},
@ -59,7 +58,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
ngOnInit() {
const component$ = this.replaceableComponents
.get$(this.data.componentKey)
.get$(this.data().componentKey)
.pipe(
filter(
(res = {} as ReplaceableComponents.ReplaceableComponent) =>
@ -102,25 +101,26 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
}
setDefaultComponentInputs() {
if (!this.defaultComponentRef || (!this.data.inputs && !this.data.outputs)) return;
if (this.data.inputs) {
for (const key in this.data.inputs) {
if (Object.prototype.hasOwnProperty.call(this.data.inputs, key)) {
if (!compare(this.defaultComponentRef[key], this.data.inputs[key].value)) {
this.defaultComponentRef[key] = this.data.inputs[key].value;
const data = this.data();
if (!this.defaultComponentRef || (!data.inputs && !data.outputs)) return;
if (data.inputs) {
for (const key in data.inputs) {
if (Object.prototype.hasOwnProperty.call(data.inputs, key)) {
if (!compare(this.defaultComponentRef[key], data.inputs[key].value)) {
this.defaultComponentRef[key] = data.inputs[key].value;
}
}
}
}
if (this.data.outputs) {
for (const key in this.data.outputs) {
if (Object.prototype.hasOwnProperty.call(this.data.outputs, key)) {
if (data.outputs) {
for (const key in data.outputs) {
if (Object.prototype.hasOwnProperty.call(data.outputs, key)) {
if (!this.defaultComponentSubscriptions[key]) {
this.defaultComponentSubscriptions[key] = this.defaultComponentRef[key].subscribe(
(value: any) => {
this.data.outputs?.[key](value);
this.data().outputs?.[key](value);
},
);
}
@ -130,24 +130,26 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
}
setProvidedData() {
this.providedData = { outputs: {}, ...this.data, inputs: {} };
this.providedData = { outputs: {}, ...this.data(), inputs: {} };
if (!this.data.inputs) return;
const data = this.data();
if (!data.inputs) return;
Object.defineProperties(this.providedData.inputs, {
...Object.keys(this.data.inputs).reduce(
...Object.keys(data.inputs).reduce(
(acc, key) => ({
...acc,
[key]: {
enumerable: true,
configurable: true,
get: () => this.data.inputs?.[key]?.value,
...(this.data.inputs?.[key]?.twoWay && {
get: () => this.data().inputs?.[key]?.value,
...(this.data().inputs?.[key]?.twoWay && {
set: (newValue: any) => {
if (this.data.inputs?.[key]) {
this.data.inputs[key].value = newValue;
const dataValue = this.data();
if (dataValue.inputs?.[key]) {
dataValue.inputs[key].value = newValue;
}
if (this.data.outputs?.[`${key}Change`]) {
this.data.outputs[`${key}Change`](newValue);
if (dataValue.outputs?.[`${key}Change`]) {
dataValue.outputs[`${key}Change`](newValue);
}
},
}),

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

6
npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html

@ -3,8 +3,8 @@
<ng-template #abpHeader>
<h3>
{{ 'AbpFeatureManagement::Features' | abpLocalization }}
@if (providerTitle) {
- {{ providerTitle }}
@if (providerTitle()) {
- {{ providerTitle() }}
}
</h3>
</ng-template>
@ -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);
<div class="mt-2" [style]="feature.style" (keyup.enter)="save()">
@switch (feature.valueType?.name) {

69
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';
@ -20,7 +20,6 @@ import {
import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs';
import { finalize } from 'rxjs/operators';
import { FreeTextInputDirective } from '../../directives';
import { FeatureManagement } from '../../models';
enum ValueTypes {
ToggleStringValueType = 'ToggleStringValueType',
@ -49,11 +48,7 @@ const DEFAULT_PROVIDER_NAME = 'D';
ModalCloseDirective,
],
})
export class FeatureManagementComponent
implements
FeatureManagement.FeatureManagementComponentInputs,
FeatureManagement.FeatureManagementComponentOutputs
{
export class FeatureManagementComponent {
protected readonly track = inject(TrackByService);
protected readonly toasterService = inject(ToasterService);
protected readonly service = inject(FeaturesService);
@ -61,14 +56,17 @@ export class FeatureManagementComponent
protected readonly confirmationService = inject(ConfirmationService);
private document = inject(DOCUMENT);
@Input()
providerKey: string;
// Signal inputs
readonly providerKey = input<string | undefined>(undefined);
readonly providerName = input<string | undefined>(undefined);
readonly providerTitle = input<string | undefined>(undefined);
readonly visibleInput = input(false, { alias: 'visible' });
@Input()
providerName: string;
// Output signals
readonly visibleChange = output<boolean>();
@Input({ required: false })
providerTitle: string;
// Internal state
protected readonly _visible = signal(false);
selectedGroupDisplayName: string;
@ -82,33 +80,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<boolean>();
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 +122,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 +155,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 +173,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 +196,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;
}
}

34
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, 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,25 @@ 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);
}
}

6
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html

@ -1,9 +1,9 @@
<abp-modal [(visible)]="visible" [busy]="modalBusy" [options]="{ size: 'lg', scrollable: false }">
@if (data.entityDisplayName || entityDisplayName) {
@if (data.entityDisplayName || entityDisplayName()) {
<ng-template #abpHeader>
<h4>
{{ 'AbpPermissionManagement::Permissions' | abpLocalization }} -
{{ entityDisplayName || data.entityDisplayName }}
{{ entityDisplayName() || data.entityDisplayName }}
</h4>
</ng-template>
<ng-template #abpBody>
@ -115,7 +115,7 @@
/>
<label class="form-check-label" [attr.for]="permission.name"
>{{ permission.displayName }}
@if (!hideBadges) {
@if (!hideBadges()) {
@for (provider of permission.grantedProviders; track $index) {
<span class="badge bg-primary text-dark"
>{{ provider.providerName }}: {{ provider.providerKey }}</span

137
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -19,18 +19,17 @@ import {
computed,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
Input,
Output,
input,
output,
QueryList,
signal,
TrackByFunction,
ViewChildren,
effect,
} from '@angular/core';
import { concat, of } from 'rxjs';
import { finalize, switchMap, take, tap } from 'rxjs/operators';
import { PermissionManagement } from '../models';
import { FormsModule } from '@angular/forms';
@ -108,59 +107,24 @@ type PermissionWithGroupName = PermissionGrantInfoDto & {
TabContent,
],
})
export class PermissionManagementComponent
implements
PermissionManagement.PermissionManagementComponentInputs,
PermissionManagement.PermissionManagementComponentOutputs
{
export class PermissionManagementComponent {
protected readonly service = inject(PermissionsService);
protected readonly configState = inject(ConfigStateService);
protected readonly toasterService = inject(ToasterService);
private document = inject(DOCUMENT);
@Input()
readonly providerName!: string;
// Signal inputs
readonly providerName = input.required<string>();
readonly providerKey = input.required<string>();
readonly hideBadges = input(false);
readonly entityDisplayName = input<string | undefined>(undefined);
readonly visibleInput = input(false, { alias: 'visible' });
@Input()
readonly providerKey!: string;
// Output signals
readonly visibleChange = output<boolean>();
@Input()
readonly hideBadges = false;
protected _visible = false;
@Input()
entityDisplayName: string | undefined;
@Input()
get visible(): boolean {
return this._visible;
}
set visible(value: boolean) {
if (value === this._visible) {
return;
}
if (value) {
this.openModal().subscribe(() => {
this._visible = true;
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible = false;
this.visibleChange.emit(false);
this.filter.set('');
}
}
@Output() readonly visibleChange = new EventEmitter<boolean>();
// Internal state
protected readonly _visible = signal(false);
@ViewChildren('selectAllInThisTabsRef')
selectAllInThisTabsRef!: QueryList<ElementRef<HTMLInputElement>>;
@ -216,6 +180,59 @@ export class PermissionManagementComponent
trackByFn: TrackByFunction<PermissionGroupDto> = (_, item) => item.name;
// Getter/setter for backward compatibility
get visible(): boolean {
return this._visible();
}
set visible(value: boolean) {
if (value === this._visible()) {
return;
}
if (value) {
this.openModal().subscribe(() => {
this._visible.set(true);
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible.set(false);
this.visibleChange.emit(false);
this.filter.set('');
}
}
constructor() {
// Sync visible input to internal signal
effect(() => {
const inputValue = this.visibleInput();
if (this._visible() !== inputValue) {
if (inputValue) {
this.openModal().subscribe(() => {
this._visible.set(true);
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
});
} else {
this.setSelectedGroup(null);
this._visible.set(false);
this.visibleChange.emit(false);
this.filter.set('');
}
}
});
}
getChecked(name: string) {
return (this.permissions.find(per => per.name === name) || { isGranted: false }).isGranted;
}
@ -249,7 +266,7 @@ export class PermissionManagementComponent
this.disableSelectAllTab = permissions.every(
permission =>
permission.isGranted &&
permission.grantedProviders?.every(p => p.providerName !== this.providerName),
permission.grantedProviders?.every(p => p.providerName !== this.providerName()),
);
} else {
this.disableSelectAllTab = false;
@ -258,7 +275,7 @@ export class PermissionManagementComponent
isGrantedByOtherProviderName(grantedProviders: ProviderInfoDto[]): boolean {
if (grantedProviders.length) {
return grantedProviders.findIndex(p => p.providerName !== this.providerName) > -1;
return grantedProviders.findIndex(p => p.providerName !== this.providerName()) > -1;
}
return false;
}
@ -348,7 +365,7 @@ export class PermissionManagementComponent
setTabCheckboxState() {
const selectablePermissions = this.selectedGroupPermissions.filter(per =>
per.grantedProviders.every(p => p.providerName === this.providerName),
per.grantedProviders.every(p => p.providerName === this.providerName()),
);
const selectedPermissions = selectablePermissions.filter(per => per.isGranted);
@ -370,7 +387,7 @@ export class PermissionManagementComponent
setGrantCheckboxState() {
const selectablePermissions = this.permissions.filter(per =>
per.grantedProviders.every(p => p.providerName === this.providerName),
per.grantedProviders.every(p => p.providerName === this.providerName()),
);
const selectedAllPermissions = selectablePermissions.filter(per => per.isGranted);
const checkboxElement = this.document.querySelector('#select-all-in-all-tabs') as any;
@ -456,7 +473,7 @@ export class PermissionManagementComponent
this.modalBusy = true;
this.service
.update(this.providerName, this.providerKey, { permissions: changedPermissions })
.update(this.providerName(), this.providerKey(), { permissions: changedPermissions })
.pipe(
switchMap(() =>
this.shouldFetchAppConfig() ? this.configState.refreshAppState() : of(null),
@ -470,11 +487,11 @@ export class PermissionManagementComponent
}
openModal() {
if (!this.providerKey || !this.providerName) {
if (!this.providerKey() || !this.providerName()) {
throw new Error('Provider Key and Provider Name are required.');
}
return this.service.get(this.providerName, this.providerKey).pipe(
return this.service.get(this.providerName(), this.providerKey()).pipe(
tap((permissionRes: GetPermissionListResultDto) => {
const { groups } = permissionRes || {};
@ -486,7 +503,7 @@ export class PermissionManagementComponent
this.disabledSelectAllInAllTabs = this.permissions.every(
per =>
per.isGranted &&
per.grantedProviders.every(provider => provider.providerName !== this.providerName),
per.grantedProviders.every(provider => provider.providerName !== this.providerName()),
);
}),
);
@ -511,9 +528,9 @@ export class PermissionManagementComponent
shouldFetchAppConfig() {
const currentUser = this.configState.getOne('currentUser') as CurrentUserDto;
if (this.providerName === 'R') return currentUser.roles.some(role => role === this.providerKey);
if (this.providerName() === 'R') return currentUser.roles.some(role => role === this.providerKey());
if (this.providerName === 'U') return currentUser.id === this.providerKey;
if (this.providerName() === 'U') return currentUser.id === this.providerKey();
return false;
}

2
npm/ng-packs/packages/setting-management/package.json

@ -12,7 +12,7 @@
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/aria": "~21.1.0"
"@angular/aria": "~21.0.0"
},
"publishConfig": {
"access": "public"

6
npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html

@ -30,7 +30,7 @@
</a>
<div #routeContainer class="dropdown-menu border-0 shadow-sm"
(click)="$event.preventDefault(); $event.stopPropagation()"
[class.d-block]="smallScreen && rootDropdownExpand[route.name]">
[class.d-block]="smallScreen() && rootDropdownExpand[route.name]">
<ng-container *ngTemplateOutlet="forTemplate; context: { $implicit: route }" />
</div>
</li>
@ -60,7 +60,7 @@
<div class="dropdown-submenu" ngbDropdown #dropdownSubmenu="ngbDropdown" placement="right-top" [autoClose]="true"
*abpPermission="child.requiredPolicy">
<div ngbDropdownToggle [class.dropdown-toggle]="false">
<a abpEllipsis="210px" [abpEllipsisEnabled]="!smallScreen" role="button"
<a abpEllipsis="210px" [abpEllipsisEnabled]="!smallScreen()" role="button"
class="btn d-block text-start dropdown-toggle">
@if (child.iconClass) {
<i [class]="child.iconClass"></i>
@ -69,7 +69,7 @@
</a>
</div>
<div #childrenContainer class="dropdown-menu dropdown-menu-start border-0 shadow-sm"
[class.d-block]="smallScreen && dropdownSubmenu.isOpen()">
[class.d-block]="smallScreen() && dropdownSubmenu.isOpen()">
<ng-container *ngTemplateOutlet="forTemplate; context: { $implicit: child }" />
</div>
</div>

4
npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts

@ -10,11 +10,11 @@ import {
Component,
ElementRef,
inject,
Input,
QueryList,
Renderer2,
TrackByFunction,
ViewChildren,
input
} from '@angular/core';
import { NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@ -39,7 +39,7 @@ export class RoutesComponent {
public readonly routesService = inject(RoutesService);
protected renderer = inject(Renderer2);
@Input() smallScreen?: boolean;
readonly smallScreen = input<boolean>(undefined);
@ViewChildren('childrenContainer') childrenContainers!: QueryList<ElementRef<HTMLDivElement>>;

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

97
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -2,13 +2,15 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
Renderer2,
ViewChild,
effect,
inject,
input,
output,
signal,
computed
} from '@angular/core';
import { ABP, StopPropagationDirective } from '@abp/ng.core';
@ -17,16 +19,16 @@ import { ABP, StopPropagationDirective } from '@abp/ng.core';
template: `
<button
#button
[id]="buttonId"
[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)"
[id]="buttonId()"
[attr.type]="buttonType()"
[attr.form]="formName()"
[class]="buttonClass()"
[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],
@ -34,54 +36,49 @@ import { ABP, StopPropagationDirective } from '@abp/ng.core';
export class ButtonComponent implements OnInit {
private renderer = inject(Renderer2);
@Input()
buttonId = '';
readonly buttonId = input('');
readonly buttonClass = input('btn btn-primary');
readonly buttonType = input('button');
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);
@Input()
buttonClass = 'btn btn-primary';
// Internal writable signal for loading state - can be set programmatically
private readonly _loading = signal(false);
@Input()
buttonType = 'button';
// Computed that combines input and internal state
readonly isLoading = computed(() => this.loadingInput() || this._loading());
@Input()
formName?: string = undefined;
@Input()
iconClass?: string;
@Input()
loading = false;
@Input()
disabled: boolean | undefined = false;
@Input()
attributes?: ABP.Dictionary<string>;
@Output() readonly click = new EventEmitter<MouseEvent>();
@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() {
if (this.attributes) {
Object.keys(this.attributes).forEach(key => {
if (this.attributes?.[key]) {
this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]);
const attributes = this.attributes();
if (attributes) {
Object.keys(attributes).forEach(key => {
if (attributes[key]) {
this.renderer.setAttribute(this.buttonRef.nativeElement, key, attributes[key]);
}
});
}

8
npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts

@ -1,13 +1,13 @@
import { Component, HostBinding, Input } from '@angular/core';
import { Component, HostBinding, input } from '@angular/core';
@Component({
selector: 'abp-card-body',
template: ` <div [class]="cardBodyClass" [style]="cardBodyStyle">
template: ` <div [class]="cardBodyClass()" [style]="cardBodyStyle()">
<ng-content></ng-content>
</div>`,
})
export class CardBodyComponent {
@HostBinding('class') componentClass = 'card-body';
@Input() cardBodyClass: string;
@Input() cardBodyStyle: string;
readonly cardBodyClass = input<string>(undefined);
readonly cardBodyStyle = input<string>(undefined);
}

8
npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts

@ -1,9 +1,9 @@
import { Component, HostBinding, Input } from '@angular/core';
import { Component, HostBinding, input } from '@angular/core';
@Component({
selector: 'abp-card-footer',
template: `
<div [style]="cardFooterStyle" [class]="cardFooterClass">
<div [style]="cardFooterStyle()" [class]="cardFooterClass()">
<ng-content></ng-content>
</div>
`,
@ -12,6 +12,6 @@ import { Component, HostBinding, Input } from '@angular/core';
})
export class CardFooterComponent {
@HostBinding('class') componentClass = 'card-footer';
@Input() cardFooterStyle: string;
@Input() cardFooterClass: string;
readonly cardFooterStyle = input<string>(undefined);
readonly cardFooterClass = input<string>(undefined);
}

8
npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts

@ -1,9 +1,9 @@
import { Component, HostBinding, Input } from '@angular/core';
import { Component, HostBinding, input } from '@angular/core';
@Component({
selector: 'abp-card-header',
template: `
<div [class]="cardHeaderClass" [style]="cardHeaderStyle">
<div [class]="cardHeaderClass()" [style]="cardHeaderStyle()">
<ng-content></ng-content>
</div>
`,
@ -12,6 +12,6 @@ import { Component, HostBinding, Input } from '@angular/core';
})
export class CardHeaderComponent {
@HostBinding('class') componentClass = 'card-header';
@Input() cardHeaderClass: string;
@Input() cardHeaderStyle: string;
readonly cardHeaderClass = input<string>(undefined);
readonly cardHeaderStyle = input<string>(undefined);
}

8
npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts

@ -1,14 +1,14 @@
import { Component, Input } from '@angular/core';
import { Component, input } from '@angular/core';
@Component({
selector: 'abp-card',
template: ` <div class="card" [class]="cardClass" [style]="cardStyle">
template: ` <div class="card" [class]="cardClass()" [style]="cardStyle()">
<ng-content></ng-content>
</div>`,
imports: [],
})
export class CardComponent {
@Input() cardClass: string;
readonly cardClass = input<string>(undefined);
@Input() cardStyle: string;
readonly cardStyle = input<string>(undefined);
}

41
npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts

@ -1,4 +1,4 @@
import { Component, EventEmitter, forwardRef, Input, Output } 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';
@ -9,16 +9,16 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
<input
type="checkbox"
[(ngModel)]="value"
[id]="checkboxId"
[readonly]="checkboxReadonly"
[class]="checkboxClass"
[style]="checkboxStyle"
(blur)="checkboxBlur.next()"
(focus)="checkboxFocus.next()"
[id]="checkboxId()"
[readonly]="checkboxReadonly()"
[class]="checkboxClass()"
[style]="checkboxStyle()"
(blur)="checkboxBlur.emit()"
(focus)="checkboxFocus.emit()"
/>
@if (label) {
<label [class]="labelClass" [for]="checkboxId">
{{ label | abpLocalization }}
@if (label()) {
<label [class]="labelClass()" [for]="checkboxId()">
{{ label() | abpLocalization }}
</label>
}
</div>
@ -33,17 +33,12 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
imports: [FormsModule, LocalizationPipe],
})
export class FormCheckboxComponent extends AbstractNgModelComponent {
@Input() label?: string;
@Input() labelClass = 'form-check-label';
@Input() checkboxId!: string;
@Input() checkboxStyle:
| {
[klass: string]: any;
}
| null
| undefined;
@Input() checkboxClass = 'form-check-input';
@Input() checkboxReadonly = false;
@Output() checkboxBlur = new EventEmitter<void>();
@Output() checkboxFocus = new EventEmitter<void>();
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 checkboxClass = input('form-check-input');
readonly checkboxReadonly = input(false);
readonly checkboxBlur = output<void>();
readonly checkboxFocus = output<void>();
}

45
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 } 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,20 +6,20 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
selector: 'abp-form-input',
template: `
<div class="mb-3">
@if (label) {
<label [class]="labelClass" [for]="inputId">
{{ label | abpLocalization }}
@if (label()) {
<label [class]="labelClass()" [for]="inputId()">
{{ label() | abpLocalization }}
</label>
}
<input
type="text"
[id]="inputId"
[placeholder]="inputPlaceholder"
[readonly]="inputReadonly"
[class]="inputClass"
[style]="inputStyle"
(blur)="formBlur.next()"
(focus)="formFocus.next()"
[id]="inputId()"
[placeholder]="inputPlaceholder()"
[readonly]="inputReadonly()"
[class]="inputClass()"
[style]="inputStyle()"
(blur)="formBlur.emit()"
(focus)="formFocus.emit()"
[(ngModel)]="value"
/>
</div>
@ -34,18 +34,13 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core';
imports: [LocalizationPipe, FormsModule],
})
export class FormInputComponent extends AbstractNgModelComponent {
@Input() inputId!: string;
@Input() inputReadonly = false;
@Input() label = '';
@Input() labelClass = 'form-label';
@Input() inputPlaceholder = '';
@Input() inputStyle:
| {
[klass: string]: any;
}
| null
| undefined;
@Input() inputClass = 'form-control';
@Output() formBlur = new EventEmitter<void>();
@Output() formFocus = new EventEmitter<void>();
readonly inputId = input.required<string>();
readonly inputReadonly = input(false);
readonly label = input('');
readonly labelClass = input('form-label');
readonly inputPlaceholder = input('');
readonly inputStyle = input<{ [klass: string]: any } | null | undefined>(undefined);
readonly inputClass = input('form-control');
readonly formBlur = output<void>();
readonly formFocus = output<void>();
}

45
npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts

@ -1,17 +1,17 @@
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, inject } 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"
[style.width.vw]="progressLevel"
[style]="{
'background-color': color,
'background-color': color(),
'box-shadow': boxShadow,
}"
></div>
@ -27,33 +27,26 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
private httpWaitService = inject(HttpWaitService);
private routerWaitService = inject(RouterWaitService);
protected _isLoading!: boolean;
readonly isLoadingInput = input(false, { alias: 'isLoading' });
readonly containerClass = input('abp-loader-bar');
readonly color = input('#77b6ff');
@Input()
set isLoading(value: boolean) {
this._isLoading = value;
this.cdRef.detectChanges();
}
get isLoading(): boolean {
return this._isLoading;
}
@Input()
containerClass = 'abp-loader-bar';
@Input()
color = '#77b6ff';
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();
@ -73,7 +66,7 @@ export class LoaderBarComponent implements OnDestroy, OnInit {
};
get boxShadow(): string {
return `0 0 10px rgba(${this.color}, 0.5)`;
return `0 0 10px rgba(${this.color()}, 0.5)`;
}
ngOnInit() {
@ -95,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);
@ -108,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/password/password.component.html

@ -2,7 +2,7 @@
<input
[type]="fieldTextType ? 'text' : 'password'"
class="form-control"
[id]="inputId"
[id]="inputId()"
[(ngModel)]="value"
/>

6
npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts

@ -1,4 +1,4 @@
import { Component, forwardRef, Input } from '@angular/core';
import { Component, forwardRef, input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractNgModelComponent } from '@abp/ng.core';
@ -21,8 +21,8 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
],
})
export class PasswordComponent extends AbstractNgModelComponent {
@Input() inputId!: string;
@Input() formControlName!: string;
readonly inputId = input.required<string>();
readonly formControlName = input.required<string>();
fieldTextType?: boolean;
toggleFieldTextType() {

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

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

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

@ -1,4 +1,4 @@
import { Component, HostListener, Input, OnInit } 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,43 +21,41 @@ export class ToastContainerComponent implements OnInit {
toasts = [] as Toaster.Toast[];
@Input()
top?: string;
@Input()
right = '30px';
defaultRight = '30px';
defaultMobileRight = '0';
@Input()
bottom = '30px';
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);
@Input()
left?: string;
protected readonly right = signal('30px');
readonly defaultRight = '30px';
readonly defaultMobileRight = '0';
@Input()
toastKey?: string;
constructor() {
effect(() => {
this.right.set(this.rightInput());
});
}
ngOnInit() {
this.setDefaultRight();
this.toasts$.subscribe(toasts => {
this.toasts = this.toastKey
this.toasts = this.toastKey()
? toasts.filter(t => {
return t.options && t.options.containerKey !== this.toastKey;
return t.options && t.options.containerKey !== this.toastKey();
})
: toasts;
});
}
@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);
}
}

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

@ -3,15 +3,15 @@
<i class="bi icon" [class]="iconClass" aria-hidden="true"></i>
</div>
<div class="abp-toast-content">
@if (toast.options?.closable) {
@if (toast().options?.closable) {
<button class="abp-toast-close-button" (click)="close()">
<i class="bi bi-x fs-4" aria-hidden="true"></i>
</button>
}
<div class="abp-toast-title">
{{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }}
{{ toast().title | abpLocalization: toast().options?.titleLocalizationParams }}
</div>
<p class="abp-toast-message"
[innerHTML]="toast.message | abpLocalization: toast.options?.messageLocalizationParams"></p>
[innerHTML]="toast().message | abpLocalization: toast().options?.messageLocalizationParams"></p>
</div>
</div>

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

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, OnInit, Output, input } from '@angular/core';
import { Toaster } from '../../models/toaster';
import { LocalizationPipe } from '@abp/ng.core';
@ -9,24 +9,24 @@ import { LocalizationPipe } from '@abp/ng.core';
imports: [LocalizationPipe],
})
export class ToastComponent implements OnInit {
@Input()
toast!: Toaster.Toast;
readonly toast = input.required<Toaster.Toast>();
@Output() remove = new EventEmitter<number>();
get severityClass(): string {
if (!this.toast || !this.toast.severity) return '';
return `abp-toast-${this.toast.severity}`;
const toast = this.toast();
if (!toast || !toast.severity) return '';
return `abp-toast-${toast.severity}`;
}
get iconClass(): string {
const { iconClass } = this.toast.options || {};
const { iconClass } = this.toast().options || {};
if (iconClass) {
return iconClass;
}
switch (this.toast.severity) {
switch (this.toast().severity) {
case 'success':
return 'bi-check';
case 'info':
@ -41,7 +41,7 @@ export class ToastComponent implements OnInit {
}
ngOnInit() {
const { sticky, life } = this.toast.options || {};
const { sticky, life } = this.toast().options || {};
if (sticky) return;
const timeout = life || 5000;
@ -51,10 +51,10 @@ export class ToastComponent implements OnInit {
}
close() {
this.remove.emit(this.toast.options?.id);
this.remove.emit(this.toast().options?.id);
}
tap() {
if (this.toast.options?.tapToDismiss) this.close();
if (this.toast().options?.tapToDismiss) this.close();
}
}

5
npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts

@ -1,4 +1,4 @@
import { Directive, Input, OnChanges, SimpleChanges, inject } from '@angular/core';
import { Directive, OnChanges, SimpleChanges, inject, input } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
@ -7,8 +7,7 @@ import { NgControl } from '@angular/forms';
export class DisabledDirective implements OnChanges {
private ngControl = inject(NgControl, { host: true });
@Input()
abpDisabled = false;
readonly abpDisabled = input(false);
// Related issue: https://github.com/angular/angular/issues/35330
ngOnChanges({ abpDisabled }: SimpleChanges) {

59
npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts

@ -1,47 +1,48 @@
import {
AfterViewInit,
ChangeDetectorRef,
Directive,
ElementRef,
HostBinding,
Input,
inject
import {
AfterViewInit,
ChangeDetectorRef,
computed,
Directive,
ElementRef,
inject,
input,
signal
} from '@angular/core';
@Directive({
selector: '[abpEllipsis]',
host: {
'[title]': 'effectiveTitle()',
'[class.abp-ellipsis-inline]': 'inlineClass()',
'[class.abp-ellipsis]': 'ellipsisClass()',
'[style.max-width]': 'maxWidth()'
}
})
export class EllipsisDirective implements AfterViewInit {
private cdRef = inject(ChangeDetectorRef);
private elRef = inject(ElementRef);
@Input('abpEllipsis')
width?: string;
readonly width = input<string | undefined>(undefined, { alias: 'abpEllipsis' });
readonly title = input<string | undefined>(undefined);
readonly enabled = input(true, { alias: 'abpEllipsisEnabled' });
@HostBinding('title')
@Input()
title?: string;
private readonly autoTitle = signal<string | undefined>(undefined);
@Input('abpEllipsisEnabled')
enabled = true;
protected readonly effectiveTitle = computed(() => this.title() || this.autoTitle());
@HostBinding('class.abp-ellipsis-inline')
get inlineClass() {
return this.enabled && this.width;
}
protected readonly inlineClass = computed(() => this.enabled() && !!this.width());
@HostBinding('class.abp-ellipsis')
get class() {
return this.enabled && !this.width;
}
protected readonly ellipsisClass = computed(() => this.enabled() && !this.width());
@HostBinding('style.max-width')
get maxWidth() {
return this.enabled && this.width ? this.width || '170px' : undefined;
}
protected readonly maxWidth = computed(() => {
const width = this.width();
return this.enabled() && width ? width || '170px' : undefined;
});
ngAfterViewInit() {
this.title = this.title || (this.elRef.nativeElement as HTMLElement).innerText;
this.cdRef.detectChanges();
if (!this.title()) {
this.autoTitle.set((this.elRef.nativeElement as HTMLElement).innerText);
this.cdRef.detectChanges();
}
}
}

73
npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts

@ -1,16 +1,16 @@
import {
ComponentFactoryResolver,
ComponentRef,
Directive,
ElementRef,
EmbeddedViewRef,
HostBinding,
Injector,
Input,
OnDestroy,
OnInit,
Renderer2,
inject
import {
ComponentRef,
Directive,
ElementRef,
EmbeddedViewRef,
Injector,
OnDestroy,
OnInit,
Renderer2,
effect,
inject,
input,
ViewContainerRef
} from '@angular/core';
import { Subscription, timer } from 'rxjs';
import { take } from 'rxjs/operators';
@ -18,29 +18,38 @@ import { LoadingComponent } from '../components';
@Directive({
selector: '[abpLoading]',
host: {
'[style.position]': '"relative"'
}
})
export class LoadingDirective implements OnInit, OnDestroy {
private elRef = inject<ElementRef<HTMLElement>>(ElementRef);
private cdRes = inject(ComponentFactoryResolver);
private injector = inject(Injector);
private renderer = inject(Renderer2);
private viewContainerRef = inject(ViewContainerRef);
private _loading!: boolean;
readonly loading = input(false, { alias: 'abpLoading' });
readonly targetElementInput = input<HTMLElement | undefined>(undefined, { alias: 'abpLoadingTargetElement' });
readonly delay = input(0, { alias: 'abpLoadingDelay' });
@HostBinding('style.position')
position = 'relative';
private targetElement: HTMLElement | undefined;
@Input('abpLoading')
get loading(): boolean {
return this._loading;
componentRef: ComponentRef<LoadingComponent> | null = null;
rootNode: HTMLDivElement | null = null;
timerSubscription: Subscription | null = null;
constructor() {
effect(() => {
const newValue = this.loading();
this.handleLoadingChange(newValue);
});
}
set loading(newValue: boolean) {
private handleLoadingChange(newValue: boolean) {
setTimeout(() => {
if (!newValue && this.timerSubscription) {
this.timerSubscription.unsubscribe();
this.timerSubscription = null;
this._loading = newValue;
if (this.rootNode) {
this.renderer.removeChild(this.rootNode.parentElement, this.rootNode);
@ -49,13 +58,13 @@ export class LoadingDirective implements OnInit, OnDestroy {
return;
}
this.timerSubscription = timer(this.delay)
this.timerSubscription = timer(this.delay())
.pipe(take(1))
.subscribe(() => {
if (!this.componentRef) {
this.componentRef = this.cdRes
.resolveComponentFactory(LoadingComponent)
.create(this.injector);
this.componentRef = this.viewContainerRef.createComponent(LoadingComponent, {
injector: this.injector
});
}
if (newValue && !this.rootNode) {
@ -66,23 +75,13 @@ export class LoadingDirective implements OnInit, OnDestroy {
this.rootNode = null;
}
this._loading = newValue;
this.timerSubscription = null;
});
}, 0);
}
@Input('abpLoadingTargetElement')
targetElement: HTMLElement | undefined;
@Input('abpLoadingDelay')
delay = 0;
componentRef!: ComponentRef<LoadingComponent>;
rootNode: HTMLDivElement | null = null;
timerSubscription: Subscription | null = null;
ngOnInit() {
this.targetElement = this.targetElementInput();
if (!this.targetElement) {
const { offsetHeight, offsetWidth } = this.elRef.nativeElement;
if (!offsetHeight && !offsetWidth && this.elRef.nativeElement.children?.length) {

6
npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts

@ -3,10 +3,10 @@ import {
AfterViewInit,
Directive,
HostBinding,
Input,
OnDestroy,
inject,
PLATFORM_ID,
input
} from '@angular/core';
import { ColumnMode, DatatableComponent } from '@swimlane/ngx-datatable';
import { fromEvent, Subscription } from 'rxjs';
@ -25,11 +25,11 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private subscription = new Subscription();
private resizeDiff = 0;
@Input() class = 'material bordered';
readonly class = input('material bordered');
@HostBinding('class')
get classes(): string {
return `ngx-datatable ${this.class}`;
return `ngx-datatable ${this.class()}`;
}
constructor() {

33
npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts

@ -1,7 +1,6 @@
import {
ChangeDetectorRef,
Directive,
Input,
OnChanges,
OnInit,
DoCheck,
@ -10,6 +9,7 @@ import {
DestroyRef,
ViewContainerRef,
Renderer2,
input
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { distinctUntilChanged } from 'rxjs';
@ -35,7 +35,7 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
protected readonly viewContainerRef = inject(ViewContainerRef);
protected readonly renderer = inject(Renderer2);
@Input() list!: ListService;
readonly list = input.required<ListService>();
constructor() {
this.setInitialValues();
@ -62,7 +62,7 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
}
protected subscribeToRequestStatus() {
const requestStatus$ = this.list.requestStatus$.pipe(distinctUntilChanged());
const requestStatus$ = this.list().requestStatus$.pipe(distinctUntilChanged());
const { emptyMessage, errorMessage } = this.ngxDatatableMessages || defaultNgxDatatableMessages;
requestStatus$.subscribe(status => {
@ -137,14 +137,15 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
this.table.sort
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ sorts: [{ prop, dir }] }) => {
if (prop === this.list.sortKey && this.list.sortOrder === 'desc') {
this.list.sortKey = '';
this.list.sortOrder = '';
const list = this.list();
if (prop === list.sortKey && list.sortOrder === 'desc') {
list.sortKey = '';
list.sortOrder = '';
this.table.sorts = [];
this.cdRef.detectChanges();
} else {
this.list.sortKey = prop;
this.list.sortOrder = dir;
list.sortKey = prop;
list.sortOrder = dir;
}
});
}
@ -156,32 +157,32 @@ export class NgxDatatableListDirective implements OnChanges, OnInit, DoCheck {
}
protected subscribeToQuery() {
this.list.query$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
const offset = this.list.page;
this.list().query$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
const offset = this.list().page;
if (this.table.offset !== offset) this.table.offset = offset;
});
}
protected setTablePage(pageNum: number) {
this.list.page = pageNum;
this.list().page = pageNum;
this.table.offset = pageNum;
}
protected refreshPageIfDataExist() {
if (this.table.rows?.length < 1 && this.table.count > 0) {
let maxPage = Math.floor(Number(this.table.count / this.list.maxResultCount));
let maxPage = Math.floor(Number(this.table.count / this.list().maxResultCount));
if (this.table.count < this.list.maxResultCount) {
if (this.table.count < this.list().maxResultCount) {
this.setTablePage(0);
return;
}
if (this.table.count % this.list.maxResultCount === 0) {
if (this.table.count % this.list().maxResultCount === 0) {
maxPage -= 1;
}
if (this.list.page < maxPage) {
this.setTablePage(this.list.page);
if (this.list().page < maxPage) {
this.setTablePage(this.list().page);
return;
}

32
npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts

@ -1,37 +1,41 @@
import { OnInit, Directive, OnDestroy, Input, ViewContainerRef, TemplateRef, inject } from '@angular/core';
import { Directive, OnDestroy, ViewContainerRef, TemplateRef, inject, input, effect } from '@angular/core';
import { EMPTY, from, Observable, of, Subscription } from 'rxjs';
type VisibleInput = boolean | Promise<boolean> | Observable<boolean> | undefined | null;
@Directive({
selector: '[abpVisible]',
})
export class AbpVisibleDirective implements OnDestroy, OnInit {
export class AbpVisibleDirective implements OnDestroy {
private viewContainerRef = inject(ViewContainerRef);
private templateRef = inject<TemplateRef<unknown>>(TemplateRef);
conditionSubscription: Subscription | undefined;
isVisible: boolean | undefined;
private conditionSubscription: Subscription | undefined;
private isVisible: boolean | undefined;
private condition$: Observable<boolean> = of(false);
@Input() set abpVisible(
value: boolean | Promise<boolean> | Observable<boolean> | undefined | null,
) {
this.condition$ = checkType(value);
this.subscribeToCondition();
}
readonly abpVisible = input<VisibleInput>();
private condition$: Observable<boolean> = of(false);
ngOnInit(): void {
this.updateVisibility();
constructor() {
effect(() => {
const value = this.abpVisible();
this.condition$ = checkType(value);
this.subscribeToCondition();
});
}
ngOnDestroy(): void {
this.conditionSubscription?.unsubscribe();
}
private subscribeToCondition() {
this.conditionSubscription?.unsubscribe();
this.conditionSubscription = this.condition$.subscribe(value => {
this.isVisible = value;
this.updateVisibility();
});
}
private updateVisibility() {
this.viewContainerRef.clear();
// it should be false not falsy
@ -42,7 +46,7 @@ export class AbpVisibleDirective implements OnDestroy, OnInit {
}
}
function checkType(value: boolean | Promise<boolean> | Observable<boolean> | undefined | null) {
function checkType(value: VisibleInput) {
if (value instanceof Promise) {
return from(value);
} else if (value instanceof Observable) {

Loading…
Cancel
Save