Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<div class="form-field-content">
<ng-content></ng-content>
</div>
<div class="form-field-focus-addon" [class.has-focus-addon]="hasFocusAddon()">
<div>
<ng-content select="step-form-field-focus-addon" />
</div>
</div>
<section class="form-field-error">
<ng-content select="step-error"> </ng-content>
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, computed, contentChild, input, Input, ViewEncapsulation } from '@angular/core';
import { AbstractControl, NgControl } from '@angular/forms';
import { getControlWarningsContainer } from '../../types/form-control-warnings-extension';
import { FormFieldFocusAddonDirective } from '../../directives/form-field-focus-addon.directive';
@Component({
selector: 'step-form-field',
templateUrl: './form-field.component.html',
Expand All @@ -25,6 +26,11 @@ export class FormFieldComponent {
/* @ContentChild(NgControl) */
protected contentControl = contentChild(NgControl);

/* @ContentChild(FormFieldFocusAddonDirective) */
private formFieldFocusAddon = contentChild(FormFieldFocusAddonDirective);

protected readonly hasFocusAddon = computed(() => this.formFieldFocusAddon());

protected readonly control = computed(() => {
const contentControl = this.contentControl()?.control;
return this.explicitControl() ?? contentControl;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<mat-slide-toggle color="primary" [ngModel]="_regexSwitcher?.isRegex" (ngModelChange)="toggleSwitch($event)" />
<span>Regex</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:host {
display: flex;
align-items: center;
justify-content: space-between;
}

span {
font-size: 11px;
}

mat-slide-toggle {
transform: scale(0.8);
}

.selected {
font-weight: 600;
text-decoration: underline;
}

::ng-deep step-form-field.ng-invalid.ng-touched div.mdc-switch__ripple {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component, inject } from '@angular/core';
import { FilterRegexSwitcherService } from '../../injectables/filter-regex-switcher.service';

@Component({
selector: 'step-input-filter-regex-switcher',
templateUrl: './input-filter-regex-switcher.component.html',
styleUrl: './input-filter-regex-switcher.component.scss',
})
export class InputFilterRegexSwitcherComponent {
protected readonly _regexSwitcher = inject(FilterRegexSwitcherService, { optional: true });

toggleSwitch(active: any) {
if (active) {
this._regexSwitcher?.switchToRegex?.();
} else {
this._regexSwitcher?.switchToText?.();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<step-form-field [matTooltip]="(invalidFilterMessage$ | async)!">
<input type="text" [formControl]="filterControl" />
@if (useRegexSwitcher()) {
<step-form-field-focus-addon>
<step-input-filter-regex-switcher />
</step-form-field-focus-addon>
}
</step-form-field>
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import {
Component,
ElementRef,
forwardRef,
inject,
Input,
OnChanges,
OnInit,
signal,
SimpleChanges,
} from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { debounceTime, filter, merge, Observable, of } from 'rxjs';
import { BaseFilterComponent } from '../base-filter/base-filter.component';
Expand All @@ -14,9 +24,21 @@ import { BaseFilterComponent } from '../base-filter/base-filter.component';
},
],
})
export class InputFilterComponent extends BaseFilterComponent<string> implements OnChanges {
export class InputFilterComponent extends BaseFilterComponent<string> implements OnChanges, OnInit {
private _elRef = inject<ElementRef<HTMLElement>>(ElementRef);

protected useRegexSwitcher = signal(false);

@Input() externalSearchValue?: string;

override ngOnInit(): void {
super.ngOnInit();
// The attribute check has been used instead of injection, because
// attempt to inject FilterConnect inside InputFilter will cause DI cycle, as
// InputFilter has been already injected in FilterConnect directive
this.useRegexSwitcher.set(this._elRef.nativeElement.hasAttribute('stepfilterconnect'));
}

ngOnChanges(changes: SimpleChanges): void {
const cExternalSearchValue = changes['externalSearchValue'];
if (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Directive } from '@angular/core';

@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'step-form-field-focus-addon',
})
export class FormFieldFocusAddonDirective {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Signal } from '@angular/core';

export abstract class FilterRegexSwitcherService {
abstract readonly isRegex: Signal<boolean>;
abstract switchToText(): void;
abstract switchToRegex(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import { ControlWarningsPipe } from './pipes/control-warnings.pipe';
import { BigNumberPipe } from './pipes/big-number.pipe';
import { BooleanFilterComponent } from './components/boolean-filter/boolean-filter.component';
import { StatusIconPipe } from './pipes/status-icon.pipe';
import { FormFieldFocusAddonDirective } from './directives/form-field-focus-addon.directive';
import { InputFilterRegexSwitcherComponent } from './components/input-filter-regex-switcher/input-filter-regex-switcher.component';

@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, StepMaterialModule, RouterModule, NgxMatSelectSearchModule],
Expand Down Expand Up @@ -119,6 +121,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe';
MarkerComponent,
AlertsContainerComponent,
InputModelFormatterDirective,
FormFieldFocusAddonDirective,
ProjectNamePipe,
GetObjectFieldPipe,
StatusCommonComponent,
Expand All @@ -128,6 +131,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe';
BigNumberPipe,
BooleanFilterComponent,
StatusIconPipe,
InputFilterRegexSwitcherComponent,
],
exports: [
CommonModule,
Expand All @@ -146,6 +150,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe';
WidthExpandersDirective,
ModalWindowComponent,
FormFieldComponent,
FormFieldFocusAddonDirective,
LabelDirective,
PrefixDirective,
SuffixDirective,
Expand Down Expand Up @@ -249,6 +254,7 @@ export * from './directives/error.directive';
export * from './directives/warning.directive';
export * from './directives/alert.directive';
export * from './directives/prevent-chars.directive';
export * from './directives/form-field-focus-addon.directive';
export * from './directives/allow-chars.directive';
export * from './directives/label-addon.directive';
export * from './injectables/array-item-label-value-extractor';
Expand Down Expand Up @@ -314,3 +320,4 @@ export * from './injectables/time-converters-factory.service';
export * from './components/simple-object-input/simple-object-input.component';
export * from './injectables/statuses-colors.token';
export * from './pipes/status-icon.pipe';
export * from './injectables/filter-regex-switcher.service';
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<table mat-table [dataSource]="tableDataSource!" [trackBy]="trackBy">
@for (searchCol of searchColumns; track searchCol.colName) {
<ng-container [matColumnDef]="searchCol.colName">
<th mat-header-cell *matHeaderCellDef>
<th mat-header-cell *matHeaderCellDef class="step-search-cell">
@if (searchCol.searchName) {
@if (searchCol.template) {
<ng-container *ngTemplateOutlet="searchCol.template" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ step-table {
width: 100%;
border-collapse: separate;

th.step-search-cell {
overflow: auto !important;
}

thead tr {
&:nth-child(1) {
top: 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,101 @@
import { AfterViewInit, DestroyRef, Directive, inject, Input, OnDestroy } from '@angular/core';
import { BaseFilterComponent, isRegexValidator } from '../../basics/step-basics.module';
import {
AfterViewInit,
computed,
DestroyRef,
Directive,
effect,
forwardRef,
inject,
input,
model,
OnDestroy,
signal,
} from '@angular/core';
import { BaseFilterComponent, FilterRegexSwitcherService, isRegexValidator } from '../../basics/step-basics.module';
import { SearchValue } from '../shared/search-value';
import { FilterCondition } from '../shared/filter-condition';
import { map } from 'rxjs';
import { SearchColumnAccessor } from '../shared/search-column-accessor';
import { FilterConditionFactoryService } from '../services/filter-condition-factory.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

type CreateConditionFn<T> = (value: T, additionalParams?: any) => FilterCondition;

@Directive({
selector: '[stepFilterConnect]',
providers: [
{
provide: FilterRegexSwitcherService,
useExisting: forwardRef(() => FilterConnectDirective),
},
],
})
export class FilterConnectDirective<T = any, CV = T> implements AfterViewInit, OnDestroy {
export class FilterConnectDirective<T = any, CV = T> implements AfterViewInit, OnDestroy, FilterRegexSwitcherService {
private _destroyRef = inject(DestroyRef);
private _filterConditionFactory = inject(FilterConditionFactoryService);
private _searchCol = inject(SearchColumnAccessor, { optional: true });
private _filter = inject<BaseFilterComponent<T, CV>>(BaseFilterComponent, { optional: true });

private isInitialized = signal(false);

private get isConnected(): boolean {
return !!this._searchCol && !!this._filter;
}

@Input() createConditionFn?: (value: T, additionalParams?: any) => FilterCondition;
@Input() conditionFnAdditionalParams?: any;
@Input() useRegex?: boolean;
/** @Input() **/
readonly createConditionFn = input<CreateConditionFn<T> | undefined>(undefined);

/** @Input() **/
readonly conditionFnAdditionalParams = input<any>(undefined);

/** @Input() **/
readonly useRegex = model<boolean>(true);

readonly hasRegex = computed(() => {
const useRegex = this.useRegex();
const hasFilterCondition = !!this.createConditionFn();
if (hasFilterCondition) {
return false;
}
return useRegex;
});

readonly isRegex = this.useRegex.asReadonly();

switchToRegex(): void {
this.useRegex.set(true);
queueMicrotask(() => this._filter?.filterControl?.updateValueAndValidity());
}

switchToText() {
this.useRegex.set(false);
queueMicrotask(() => this._filter?.filterControl?.updateValueAndValidity());
}

ngAfterViewInit(): void {
this.setupValueChanges();
this.setupFilterChanges();
this.setupFilterValidation();
this.isInitialized.set(true);
}

ngOnDestroy(): void {
this.destroyFilterValidation();
}

private effectSetupFilterValidation = effect(() => {
const isInitialized = this.isInitialized();
const hasRegex = this.hasRegex();
if (!isInitialized) {
return;
}
if (!this.isConnected) {
console.warn('[stepFilterConnect] not connected');
return;
}

this.toggleValidator(hasRegex);
});

private setupValueChanges(): void {
if (!this.isConnected) {
console.warn('[stepFilterConnect] not connected');
Expand All @@ -45,7 +107,12 @@ export class FilterConnectDirective<T = any, CV = T> implements AfterViewInit, O
map((searchValue) => this.convertToFilterValue(searchValue)),
takeUntilDestroyed(this._destroyRef),
)
.subscribe((filterValue) => this._filter!.assignValue(filterValue));
.subscribe(({ value, regex }) => {
if (regex !== undefined) {
this.useRegex.set(regex);
}
this._filter!.assignValue(value);
});
}

private setupFilterChanges(): void {
Expand All @@ -59,46 +126,45 @@ export class FilterConnectDirective<T = any, CV = T> implements AfterViewInit, O
).subscribe((searchValue) => this._searchCol!.search(searchValue));
}

private setupFilterValidation(): void {
if (!this.isConnected) {
console.warn('[stepFilterConnect] not connected');
return;
}
if (this.useRegex || !this.createConditionFn) {
this._filter?.filterControl.addValidators(isRegexValidator);
}
}

private destroyFilterValidation(): void {
if (!this.isConnected) {
return;
}
this._filter?.filterControl.removeValidators(isRegexValidator);
this.toggleValidator(false);
}

private convertToSearchValue(value: T): SearchValue {
if (this.createConditionFn) {
return this.createConditionFn.call(this._filterConditionFactory, value, this.conditionFnAdditionalParams);
}

if (this.useRegex !== undefined) {
return { value: value as string, regex: this.useRegex };
const createConditionFn = this.createConditionFn();
if (createConditionFn) {
return createConditionFn.call(this._filterConditionFactory, value, this.conditionFnAdditionalParams());
}

return value as string;
return { value: value as string, regex: this.hasRegex() };
}

private convertToFilterValue(value?: SearchValue): T | undefined {
private convertToFilterValue(value?: SearchValue): { value: T | undefined; regex?: boolean } {
if (!value) {
return undefined;
return { value: undefined };
}
if (typeof value === 'string') {
return value as T;
return { value: value as T };
}
if (value instanceof FilterCondition) {
return value.getSearchValue() as T;
return { value: value.getSearchValue() as T };
}

return value?.value as T;
return { value: value?.value as T, regex: value?.regex };
}

private toggleValidator(isActive: boolean): void {
const filterControl = this._filter?.filterControl;
if (!filterControl) {
return;
}
if (isActive && !filterControl.hasValidator(isRegexValidator)) {
filterControl.addValidators(isRegexValidator);
} else if (!isActive && filterControl.hasValidator(isRegexValidator)) {
filterControl.removeValidators(isRegexValidator);
}
}
}
Loading