diff --git a/src/index.ts b/src/index.ts index d6bd5ca..2014a20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ export * from './lib/dialog'; export * from './lib/form'; export * from './lib/listing'; export * from './lib/help-mode'; -export * from './lib/inputs'; export * from './lib/services'; export * from './lib/loading'; export * from './lib/error'; diff --git a/src/lib/filtering/filters.module.ts b/src/lib/filtering/filters.module.ts index 7667ea0..ec8e941 100644 --- a/src/lib/filtering/filters.module.ts +++ b/src/lib/filtering/filters.module.ts @@ -1,14 +1,16 @@ -import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { TranslateModule } from '@ngx-translate/core'; -import { ChevronButtonComponent, IconButtonComponent } from '../buttons'; +import { ChevronButtonComponent } from '../buttons/chevron-button/chevron-button.component'; +import { IconButtonComponent } from '../buttons/icon-button/icon-button.component'; +import { PreventDefaultDirective } from '../directives/prevent-default.directive'; +import { StopPropagationDirective } from '../directives/stop-propagation.directive'; +import { InputWithActionComponent } from '../inputs/input-with-action/input-with-action.component'; import { QuickFiltersComponent } from './quick-filters/quick-filters.component'; import { SingleFilterComponent } from './single-filter/single-filter.component'; -import { MatIconModule } from '@angular/material/icon'; -import { PreventDefaultDirective, StopPropagationDirective } from '../directives'; -import { InputWithActionComponent } from '../inputs'; const components = [QuickFiltersComponent, SingleFilterComponent]; diff --git a/src/lib/filtering/simple-popup-filter/simple-popup-filter.component.ts b/src/lib/filtering/simple-popup-filter/simple-popup-filter.component.ts index 47fa481..bdef5b2 100644 --- a/src/lib/filtering/simple-popup-filter/simple-popup-filter.component.ts +++ b/src/lib/filtering/simple-popup-filter/simple-popup-filter.component.ts @@ -1,11 +1,11 @@ import { Component, computed, effect, input, output, signal, untracked } from '@angular/core'; +import { MatCheckbox } from '@angular/material/checkbox'; import { MatMenuModule } from '@angular/material/menu'; import { TranslateModule } from '@ngx-translate/core'; -import { MatCheckbox } from '@angular/material/checkbox'; import { ChevronButtonComponent, CircleButtonComponent, IconButtonComponent } from '../../buttons'; import { StopPropagationDirective } from '../../directives'; -import { InputWithActionComponent } from '../../inputs'; +import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component'; import { SimpleFilterOption } from '../models/simple-filter-option'; @Component({ diff --git a/src/lib/inputs/details-radio/details-radio.component.html b/src/lib/inputs/details-radio/details-radio.component.html index 4a61fbd..f374875 100644 --- a/src/lib/inputs/details-radio/details-radio.component.html +++ b/src/lib/inputs/details-radio/details-radio.component.html @@ -1,13 +1,13 @@ -
- @for (option of options; track option) { +
+ @for (option of options(); track option) {
@if (option.icon) { @@ -15,7 +15,9 @@
+ {{ option.description | translate: option.descriptionParams | replaceNbsp }} + @if (option.extraOption && !option.extraOption.hidden && isSelected(option)) {
{{ option.extraOption.label | translate }} + @if (option.extraOption.description) { }
+ @if (isSelected(option)) { } @@ -43,8 +47,10 @@ } @else {
+
+ {{ option.description | translate }} }
diff --git a/src/lib/inputs/details-radio/details-radio.component.ts b/src/lib/inputs/details-radio/details-radio.component.ts index 8d0f087..371f2c4 100644 --- a/src/lib/inputs/details-radio/details-radio.component.ts +++ b/src/lib/inputs/details-radio/details-radio.component.ts @@ -1,5 +1,5 @@ import { NgClass } from '@angular/common'; -import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; +import { booleanAttribute, Component, input, output } from '@angular/core'; import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; @@ -40,12 +40,12 @@ import { DetailsRadioOption } from './details-radio-option'; ], }) export class DetailsRadioComponent extends FormFieldComponent> { - @Input({ required: true }) options: DetailsRadioOption[] = []; - @Input({ transform: booleanAttribute }) displayInRow = false; + readonly options = input.required[]>(); + readonly displayInRow = input(false, { transform: booleanAttribute }); - @Output() readonly extraOptionChanged: EventEmitter> = new EventEmitter(); + readonly extraOptionChanged = output>(); - toggleOption(option: DetailsRadioOption): void { + toggleOption(option: DetailsRadioOption) { if (option.value !== this._value?.value && !option.disabled) { this.markAsTouched(); const currentlyChecked = this.value?.value === option.value; @@ -54,15 +54,20 @@ export class DetailsRadioComponent extends FormFieldComponent): string { + groupId(option: DetailsRadioOption) { return (option.id ?? option.label.replace('.', '-')) + '-checkbox-details-input'; } - isSelected(option: DetailsRadioOption): boolean { + isSelected(option: DetailsRadioOption) { return option.value === this.value?.value; } - emitExtraOption(): void { + emitExtraOption() { + if (!this.value) { + console.error('Extra option selected but the value is undefined'); + return; + } + this.extraOptionChanged.emit(this.value); } } diff --git a/src/lib/inputs/dynamic-input/dynamic-input.component.html b/src/lib/inputs/dynamic-input/dynamic-input.component.html index f655490..e70597e 100644 --- a/src/lib/inputs/dynamic-input/dynamic-input.component.html +++ b/src/lib/inputs/dynamic-input/dynamic-input.component.html @@ -1,37 +1,46 @@ -
- @if (label) { - +
+ @if (label()) { + } - @if (isDate) { + @if (isDate()) { + + } - @if (isText) { + @if (isText()) { } - @if (isNumber) { - + @if (isNumber()) { + }
diff --git a/src/lib/inputs/dynamic-input/dynamic-input.component.ts b/src/lib/inputs/dynamic-input/dynamic-input.component.ts index 8a8a645..acd0ce8 100644 --- a/src/lib/inputs/dynamic-input/dynamic-input.component.ts +++ b/src/lib/inputs/dynamic-input/dynamic-input.component.ts @@ -1,13 +1,13 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { FormFieldComponent } from '../form-field/form-field-component.directive'; import { NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core'; +import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatDatepickerModule } from '@angular/material/datepicker'; -import { StopPropagationDirective } from '../../directives'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { StopPropagationDirective } from '../../directives'; +import { FormFieldComponent } from '../form-field/form-field-component.directive'; -const InputTypes = { +export const InputTypes = { DATE: 'DATE', NUMBER: 'NUMBER', TEXT: 'TEXT', @@ -39,28 +39,20 @@ type DynamicInput = number | string | Date; imports: [NgClass, FormsModule, MatDatepickerModule, StopPropagationDirective, MatIconModule, MatInputModule], }) export class DynamicInputComponent extends FormFieldComponent { - @Input() label?: string; - @Input({ required: true }) type!: InputType; - @Input() placeholder?: string; - @Input() id?: string; - @Input() classList = ''; - @Input() input!: DynamicInput; - @Output() readonly closedDatepicker = new EventEmitter(); + readonly label = input(); + readonly type = input.required(); + readonly placeholder = input(); + readonly id = input(); + readonly classList = input(''); + readonly input = model(); + readonly closedDatepicker = output(); - get isDate() { - return this.type === InputTypes.DATE; - } - - get isNumber() { - return this.type === InputTypes.NUMBER; - } - - get isText() { - return this.type === InputTypes.TEXT; - } + readonly isDate = computed(() => this.type() === InputTypes.DATE); + readonly isNumber = computed(() => this.type() === InputTypes.NUMBER); + readonly isText = computed(() => this.type() === InputTypes.TEXT); writeValue(input: DynamicInput): void { - this.input = input; + this.input.set(input); } onCloseDatepicker() { diff --git a/src/lib/inputs/editable-input/editable-input.component.html b/src/lib/inputs/editable-input/editable-input.component.html index 581d1d3..0b35ec8 100644 --- a/src/lib/inputs/editable-input/editable-input.component.html +++ b/src/lib/inputs/editable-input/editable-input.component.html @@ -1,48 +1,53 @@ -@if (!editing) { - @if (showPreview) { -
- {{ value }} -
+@if (!_editing()) { + @if (showPreview()) { +
{{ value() }}
} +
- @if (canEdit) { + @if (canEdit()) { } +
-} - -@if (editing) { +} @else {
-
- @if (!parentId) { - +
+ @if (!parentId()) { + } @else { }
+
- + +
diff --git a/src/lib/inputs/editable-input/editable-input.component.ts b/src/lib/inputs/editable-input/editable-input.component.ts index 942e2d2..ce4e4da 100644 --- a/src/lib/inputs/editable-input/editable-input.component.ts +++ b/src/lib/inputs/editable-input/editable-input.component.ts @@ -1,43 +1,41 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, input, OnChanges, output, signal, SimpleChanges } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { CircleButtonComponent, CircleButtonType, CircleButtonTypes } from '../../buttons'; +import { CircleButtonComponent } from '../../buttons/circle-button/circle-button.component'; +import { CircleButtonType, CircleButtonTypes } from '../../buttons/types/circle-button.type'; @Component({ - selector: 'iqser-editable-input [value]', + selector: 'iqser-editable-input', templateUrl: './editable-input.component.html', styleUrls: ['./editable-input.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CircleButtonComponent, FormsModule], + host: { + '[class.editing]': '_editing()', + }, }) export class EditableInputComponent implements OnChanges { - @Input() id?: string; - @Input() parentId?: string; - @Input() value!: string; - @Input() editTooltip?: string; - @Input() saveTooltip?: string; - @Input() cancelTooltip?: string; - @Input() placeholder = ''; - @Input() class?: string; - @Input() showPreview = true; - @Input() canEdit = true; - @Input() buttonsType: CircleButtonType = CircleButtonTypes.default; - @Input() helpModeKey: string = ''; - @Input() lastChild = false; - @Output() readonly save = new EventEmitter(); + protected readonly _editing = signal(false); + readonly id = input(); + readonly parentId = input(); + readonly value = input.required(); + readonly editTooltip = input(''); + readonly saveTooltip = input(''); + readonly cancelTooltip = input(''); + readonly placeholder = input(''); + readonly class = input(); + readonly showPreview = input(true, { transform: booleanAttribute }); + readonly canEdit = input(true, { transform: booleanAttribute }); + readonly buttonsType = input(CircleButtonTypes.default); + readonly helpModeKey = input(''); + readonly lastChild = input(false, { transform: booleanAttribute }); + readonly save = output(); textArea?: { width: number; height: number }; newValue = ''; - private _editing = false; - - @HostBinding('class.editing') - get editing(): boolean { - return this._editing; - } - set editing(value: boolean) { - this._editing = value; - this.newValue = this.value; + this._editing.set(value); + this.newValue = this.value(); } ngOnChanges(changes: SimpleChanges): void { @@ -51,12 +49,12 @@ export class EditableInputComponent implements OnChanges { setTextAreaSize() { setTimeout(() => { - const element = document.getElementById(this.id as string) as HTMLElement; - const parentElement = document.getElementById(this.parentId as string) as HTMLElement; + const element = document.getElementById(this.id() as string) as HTMLElement; + const parentElement = document.getElementById(this.parentId() as string) as HTMLElement; const width = parentElement.offsetWidth - 98; - let height = (this.lastChild ? parentElement.offsetHeight : element.offsetHeight) - 16; - if (this.lastChild) { - const lastChildIndex = Number(this.id?.split('-')[2]); + let height = (this.lastChild() ? parentElement.offsetHeight : element.offsetHeight) - 16; + if (this.lastChild()) { + const lastChildIndex = Number(this.id()?.split('-')[2]); height = height - lastChildIndex * element.offsetHeight; } this.textArea = { width, height }; diff --git a/src/lib/inputs/index.ts b/src/lib/inputs/index.ts deleted file mode 100644 index 27194c4..0000000 --- a/src/lib/inputs/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './round-checkbox/round-checkbox.component'; -export * from './editable-input/editable-input.component'; -export * from './input-with-action/input-with-action.component'; -export * from './details-radio/details-radio.component'; -export * from './details-radio/details-radio-option'; -export * from './form-field/form-field-component.directive'; -export * from './dynamic-input/dynamic-input.component'; diff --git a/src/lib/inputs/input-with-action/input-with-action.component.html b/src/lib/inputs/input-with-action/input-with-action.component.html index f574a68..d1ea96a 100644 --- a/src/lib/inputs/input-with-action/input-with-action.component.html +++ b/src/lib/inputs/input-with-action/input-with-action.component.html @@ -1,34 +1,33 @@ -
+ - @if (hint) { - {{ hint }} + @if (hint()) { + {{ hint() }} } - @if (isSearch && !hasContent) { + @if (_isSearch() && !_hasContent()) { } - @if (isSearch && hasContent) { - + @if (_isSearch() && _hasContent()) { + } - @if (!isSearch) { + @if (icon(); as icon) { }
diff --git a/src/lib/inputs/input-with-action/input-with-action.component.ts b/src/lib/inputs/input-with-action/input-with-action.component.ts index 5e8d67f..9753394 100644 --- a/src/lib/inputs/input-with-action/input-with-action.component.ts +++ b/src/lib/inputs/input-with-action/input-with-action.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; -import { randomString } from '../../utils'; +import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; - -import { CircleButtonComponent } from '../../buttons'; import { MatIconModule } from '@angular/material/icon'; +import { CircleButtonComponent } from '../../buttons/circle-button/circle-button.component'; +import { randomString } from '../../utils/functions'; + @Component({ selector: 'iqser-input-with-action', templateUrl: './input-with-action.component.html', @@ -14,41 +14,27 @@ import { MatIconModule } from '@angular/material/icon'; imports: [FormsModule, MatIconModule, CircleButtonComponent], }) export class InputWithActionComponent { - @Input() inputId = `${randomString() + '-search-input'}`; - @Input() actionButtonId = `${randomString() + '-action-input'}`; - @Input() placeholder = ''; - @Input() hint?: string; - @Input() width: number | 'full' = 250; - @Input() icon?: string; - @Input() autocomplete: 'on' | 'off' = 'on'; - @Input() value = ''; - @Input() disabled = false; - @Output() readonly action = new EventEmitter(); - @Output() readonly valueChange = new EventEmitter(); + readonly inputId = input(`${randomString() + '-search-input'}`); + readonly actionButtonId = input(`${randomString() + '-action-input'}`); + readonly placeholder = input(''); + readonly hint = input(); + readonly width = input(250); + protected readonly _computedWidth = computed(() => (this.width() === 'full' ? '100%' : `${this.width()}px`)); + readonly icon = input(); + protected readonly _isSearch = computed(() => !this.icon()); + readonly autocomplete = input<'on' | 'off'>('on'); + readonly value = model(''); + protected readonly _hasContent = computed(() => !!this.value()?.length); + readonly disabled = input(false, { transform: booleanAttribute }); + readonly action = output(); - get hasContent(): boolean { - return !!this.value?.length; + reset() { + this.value.set(''); } - get computedWidth(): string { - return this.width === 'full' ? '100%' : `${this.width}px`; - } - - get isSearch(): boolean { - return !this.icon; - } - - constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {} - - reset(): void { - this.value = ''; - this.valueChange.emit(this.value); - this._changeDetectorRef.markForCheck(); - } - - executeAction(): void { - if (this.hasContent) { - this.action.emit(this.value); + executeAction() { + if (this._hasContent()) { + this.action.emit(this.value()); } } } diff --git a/src/lib/inputs/round-checkbox/round-checkbox.component.html b/src/lib/inputs/round-checkbox/round-checkbox.component.html index 1d9ff37..189049e 100644 --- a/src/lib/inputs/round-checkbox/round-checkbox.component.html +++ b/src/lib/inputs/round-checkbox/round-checkbox.component.html @@ -1,15 +1,16 @@
- @if (active && !indeterminate) { + @if (active() && !indeterminate()) { } - @if (indeterminate) { + + @if (indeterminate()) { }
diff --git a/src/lib/inputs/round-checkbox/round-checkbox.component.ts b/src/lib/inputs/round-checkbox/round-checkbox.component.ts index c54dab1..270ca90 100644 --- a/src/lib/inputs/round-checkbox/round-checkbox.component.ts +++ b/src/lib/inputs/round-checkbox/round-checkbox.component.ts @@ -1,4 +1,4 @@ -import { booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core'; +import { booleanAttribute, ChangeDetectionStrategy, Component, effect, ElementRef, input, numberAttribute, viewChild } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; @Component({ @@ -9,18 +9,17 @@ import { MatIconModule } from '@angular/material/icon'; standalone: true, imports: [MatIconModule], }) -export class RoundCheckboxComponent implements OnInit { - @Input() size = 20; - @Input({ transform: booleanAttribute }) active = false; - @Input() indeterminate = false; - @Input() type: 'default' | 'with-bg' = 'default'; - @HostBinding('class.disabled') - @Input() - disabled = false; +export class RoundCheckboxComponent { + protected readonly _wrapper = viewChild.required('wrapper', { read: ElementRef }); + readonly size = input(20, { transform: numberAttribute }); + readonly active = input(false, { transform: booleanAttribute }); + readonly indeterminate = input(false, { transform: booleanAttribute }); + readonly type = input<'default' | 'with-bg'>('default'); + readonly disabled = input(false, { transform: booleanAttribute }); - @ViewChild('wrapper', { static: true }) private readonly _wrapper!: ElementRef; - - ngOnInit(): void { - (this._wrapper.nativeElement as HTMLElement).style.setProperty('--size', `${this.size}px`); + constructor() { + effect(() => { + (this._wrapper().nativeElement as HTMLElement).style.setProperty('--size', `${this.size()}px`); + }); } } diff --git a/src/lib/listing/listing.module.ts b/src/lib/listing/listing.module.ts index e844f29..cf3608e 100644 --- a/src/lib/listing/listing.module.ts +++ b/src/lib/listing/listing.module.ts @@ -10,7 +10,8 @@ import { CircleButtonComponent, IconButtonComponent } from '../buttons'; import { HasScrollbarDirective, SyncWidthDirective } from '../directives'; import { EmptyStateComponent } from '../empty-state'; import { IqserFiltersModule, PopupFilterComponent } from '../filtering'; -import { InputWithActionComponent, RoundCheckboxComponent } from '../inputs'; +import { InputWithActionComponent } from '../inputs/input-with-action/input-with-action.component'; +import { RoundCheckboxComponent } from '../inputs/round-checkbox/round-checkbox.component'; import { SnakeCasePipe } from '../pipes/snake-case.pipe'; import { PageHeaderComponent } from './page-header/page-header.component'; import { ScrollButtonComponent } from './scroll-button/scroll-button.component'; diff --git a/src/lib/listing/table-header/table-header.component.ts b/src/lib/listing/table-header/table-header.component.ts index 74cc476..46b4b19 100644 --- a/src/lib/listing/table-header/table-header.component.ts +++ b/src/lib/listing/table-header/table-header.component.ts @@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, inject, Input, TemplateRef } from ' import { TranslateModule } from '@ngx-translate/core'; import { SyncWidthDirective } from '../../directives'; import { FilterService, IqserFiltersModule } from '../../filtering'; -import { RoundCheckboxComponent } from '../../inputs'; +import { RoundCheckboxComponent } from '../../inputs/round-checkbox/round-checkbox.component'; import { Id, IListable, ListingMode, ListingModes, TableColumnConfig } from '../models'; import { EntitiesService, ListingService } from '../services'; import { TableColumnNameComponent } from '../table-column-name/table-column-name.component'; diff --git a/src/lib/listing/workflow/column-header/column-header.component.ts b/src/lib/listing/workflow/column-header/column-header.component.ts index 3de5eec..43ce9dc 100644 --- a/src/lib/listing/workflow/column-header/column-header.component.ts +++ b/src/lib/listing/workflow/column-header/column-header.component.ts @@ -14,11 +14,13 @@ import { import { TranslateModule } from '@ngx-translate/core'; import { combineLatest } from 'rxjs'; import { filter, map, tap } from 'rxjs/operators'; -import { CircleButtonComponent, CircleButtonTypes } from '../../../buttons'; -import { RoundCheckboxComponent } from '../../../inputs'; -import { ContextComponent, Debounce } from '../../../utils'; -import { IListable } from '../../models'; -import { ListingService } from '../../services'; +import { CircleButtonComponent } from '../../../buttons/circle-button/circle-button.component'; +import { CircleButtonTypes } from '../../../buttons/types/circle-button.type'; +import { RoundCheckboxComponent } from '../../../inputs/round-checkbox/round-checkbox.component'; +import { ContextComponent } from '../../../utils/context.component'; +import { Debounce } from '../../../utils/decorators/debounce.decorator'; +import { IListable } from '../../models/listable'; +import { ListingService } from '../../services/listing.service'; import { WorkflowColumn } from '../models/workflow-column.model'; interface ColumnHeaderContext { diff --git a/src/lib/utils/context.component.ts b/src/lib/utils/context.component.ts index cd89a04..9100766 100644 --- a/src/lib/utils/context.component.ts +++ b/src/lib/utils/context.component.ts @@ -2,6 +2,9 @@ import { combineLatest, Observable, of } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { ValuesOf } from './types/utility-types'; +/** + * @deprecated Switch to signals instead + */ export class ContextComponent { componentContext$: Observable | null = of({} as T);