mirror of
https://github.com/valitydev/ng-libs.git
synced 2024-11-06 00:35:21 +00:00
Add lazy column for table2 (#78)
This commit is contained in:
parent
2b0d028e67
commit
784928f164
@ -5,6 +5,7 @@
|
||||
- [Prettier Config](/projects/prettier-config)
|
||||
- [ESLint Config](/projects/eslint-config)
|
||||
- [CSpell Config](/projects/cspell-config)
|
||||
- [Docs](/projects/ng-libs-doc)
|
||||
|
||||
## 💻 Development with locally built/runnable library
|
||||
|
||||
|
@ -12,7 +12,7 @@ import { getHintText } from '../utils/get-hint-text';
|
||||
styleUrls: ['./select-field.component.scss'],
|
||||
providers: createControlProviders(() => SelectFieldComponent),
|
||||
})
|
||||
export class SelectFieldComponent<T> extends FormControlSuperclass<T[]> {
|
||||
export class SelectFieldComponent<T = unknown> extends FormControlSuperclass<T[]> {
|
||||
@Input() options: Option<T>[] = [];
|
||||
@Output() searchChange = new EventEmitter<string>();
|
||||
|
||||
|
@ -17,8 +17,7 @@ import { createIntersectionObserver } from '../utils/create-intersection-observe
|
||||
standalone: true,
|
||||
})
|
||||
export class InfinityScrollDirective implements OnInit {
|
||||
vInfinityScroll = input(true, { transform: booleanAttribute });
|
||||
vInfinityScrollProgress = input(false, { transform: booleanAttribute });
|
||||
vInfinityScroll = input(false, { transform: booleanAttribute });
|
||||
vInfinityScrollMore = output();
|
||||
|
||||
constructor(
|
||||
@ -29,7 +28,7 @@ export class InfinityScrollDirective implements OnInit {
|
||||
ngOnInit() {
|
||||
createIntersectionObserver(this.elementRef.nativeElement)
|
||||
.pipe(
|
||||
filter(() => !this.vInfinityScrollProgress()),
|
||||
filter(() => this.vInfinityScroll()),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe(() => {
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
output,
|
||||
ChangeDetectionStrategy,
|
||||
model,
|
||||
} from '@angular/core';
|
||||
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@ -17,7 +17,7 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { startWith, map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { ComponentChanges } from '../../../utils';
|
||||
import { arrayAttribute, ArrayAttributeTransform, ComponentChanges } from '../../../utils';
|
||||
|
||||
import { BaseColumnComponent } from './base-column.component';
|
||||
|
||||
@ -30,7 +30,9 @@ import { BaseColumnComponent } from './base-column.component';
|
||||
<th *matHeaderCellDef [class]="columnClasses" mat-header-cell>
|
||||
<mat-checkbox
|
||||
[checked]="selection.hasValue() && (isAllSelected$ | async)"
|
||||
[disabled]="!!progress() || (filtered() && !(isAllSelected$ | async))"
|
||||
[disabled]="
|
||||
!!progress() || !data()?.length || (filtered() && !(isAllSelected$ | async))
|
||||
"
|
||||
[indeterminate]="selection.hasValue() && !(isAllSelected$ | async)"
|
||||
(change)="$event ? toggleAllRows() : null"
|
||||
>
|
||||
@ -66,10 +68,10 @@ export class SelectColumnComponent<T>
|
||||
extends BaseColumnComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
selected = input<T[]>([]);
|
||||
selectedChange = output<T[]>();
|
||||
|
||||
data = input<T[]>([]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selected = model<any[]>([]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data = input<T[], ArrayAttributeTransform<any>>([], { transform: arrayAttribute });
|
||||
progress = input<boolean | number | null | undefined>(false);
|
||||
/**
|
||||
* @deprecated
|
||||
@ -92,7 +94,7 @@ export class SelectColumnComponent<T>
|
||||
override ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.selection.changed.pipe(takeUntilDestroyed(this.dr)).subscribe(() => {
|
||||
this.selectedChange.emit(this.selection.selected);
|
||||
this.selected.set(this.selection.selected);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,21 +2,20 @@
|
||||
|
||||
<div class="wrapper">
|
||||
<v-table-info-bar
|
||||
[(filter)]="filter"
|
||||
[count]="(dataSource.data$ | async)?.length"
|
||||
[dataProgress]="!!(columnsDataProgress$ | async) && !hasMore()"
|
||||
[filter]="(filter$ | async) ?? ''"
|
||||
[filteredCount]="count$ | async"
|
||||
[filteredCount]="displayedCount$ | async"
|
||||
[hasInputs]="!!tableInputsContent?.nativeElement"
|
||||
[hasMore]="hasMore()"
|
||||
[isPreload]="isPreload()"
|
||||
[noDownload]="noDownload()"
|
||||
[preloadSize]="maxSize()"
|
||||
[progress]="progress()"
|
||||
[selectedCount]="(selected$ | async)?.length"
|
||||
[selectedCount]="rowSelected()?.length"
|
||||
[size]="size()"
|
||||
[standaloneFilter]="standaloneFilter()"
|
||||
(downloadCsv)="downloadCsv()"
|
||||
(filterChange)="filter$.next($event)"
|
||||
(load)="load()"
|
||||
(preload)="preload()"
|
||||
>
|
||||
@ -24,7 +23,10 @@
|
||||
<v-table-inputs><ng-content select="v-table-inputs"></ng-content></v-table-inputs>
|
||||
</v-table-info-bar>
|
||||
<mat-card #scrollViewport class="card">
|
||||
<v-no-records [noRecords]="!(count$ | async)" [progress]="progress()"></v-no-records>
|
||||
<v-no-records
|
||||
[noRecords]="!(displayedCount$ | async)"
|
||||
[progress]="progress()"
|
||||
></v-no-records>
|
||||
<table
|
||||
#matTable
|
||||
[dataSource]="dataSource"
|
||||
@ -32,15 +34,14 @@
|
||||
[matSortDirection]="sort().direction"
|
||||
mat-table
|
||||
matSort
|
||||
(matSortChange)="this.sortChange.emit($event)"
|
||||
(matSortChange)="sort.set($event)"
|
||||
>
|
||||
@if (rowSelectable()) {
|
||||
<v-select-column
|
||||
[data]="$any(displayedData$ | async) ?? []"
|
||||
[(selected)]="rowSelected"
|
||||
[data]="displayedData$ | async"
|
||||
[name]="columnDefs.select"
|
||||
[progress]="progress()"
|
||||
[selected]="$any(selected$ | async) ?? []"
|
||||
(selectedChange)="rowSelectedChange.emit($event)"
|
||||
></v-select-column>
|
||||
}
|
||||
@for (col of displayedNormColumns$ | async; track col; let colIndex = $index) {
|
||||
@ -59,7 +60,7 @@
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
[disabled]="(col.sort | async) === false || hasMore()"
|
||||
[disabled]="(col.sort | async) === false || hasMore() || progress()"
|
||||
[mat-sort-header]="col.field"
|
||||
[ngClass]="columnClasses"
|
||||
mat-header-cell
|
||||
@ -68,7 +69,7 @@
|
||||
</th>
|
||||
|
||||
<ng-template let-element let-rowIndex="index" matCellDef>
|
||||
@let cell = columnsData?.get?.(element)?.[colIndex];
|
||||
@let cell = columnsData?.get?.(element)?.get(col);
|
||||
<td
|
||||
[ngClass]="columnClasses"
|
||||
[ngStyle]="col?.params?.style"
|
||||
@ -78,8 +79,11 @@
|
||||
<v-value
|
||||
[emptySymbol]="!(cell?.isChild && !col?.child)"
|
||||
[highlight]="filter$ | async"
|
||||
[lazyValue]="cell?.lazyValue"
|
||||
[lazyVisible]="loadedLazyItems.has(element)"
|
||||
[value]="cell?.value | async"
|
||||
inline
|
||||
(lazyVisibleChange)="loadedLazyItems.set(element, true)"
|
||||
></v-value>
|
||||
</td>
|
||||
</ng-template>
|
||||
@ -94,9 +98,12 @@
|
||||
<tr
|
||||
*matFooterRowDef="(hasAutoShowMore$ | async) ? (displayedColumns$ | async) : []"
|
||||
[ngClass]="{ row__hidden: !(hasAutoShowMore$ | async) }"
|
||||
[vInfinityScrollProgress]="progress()"
|
||||
[vInfinityScroll]="
|
||||
!progress() &&
|
||||
!(columnsDataProgress$ | async) &&
|
||||
(filteredSortData$ | async)?.length
|
||||
"
|
||||
mat-row
|
||||
vInfinityScroll
|
||||
(vInfinityScrollMore)="showMore()"
|
||||
></tr>
|
||||
</table>
|
||||
|
@ -1,5 +1,5 @@
|
||||
:host {
|
||||
min-height: 300px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@ -11,6 +11,7 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
transform: translateZ(0);
|
||||
|
||||
@ -46,6 +47,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-footer-cell {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.row__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ContentChild,
|
||||
model,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
||||
@ -31,20 +33,32 @@ import {
|
||||
forkJoin,
|
||||
BehaviorSubject,
|
||||
debounceTime,
|
||||
share,
|
||||
first,
|
||||
merge,
|
||||
tap,
|
||||
defer,
|
||||
} from 'rxjs';
|
||||
import { shareReplay, map, distinctUntilChanged, delay, filter, startWith } from 'rxjs/operators';
|
||||
import {
|
||||
shareReplay,
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
delay,
|
||||
filter,
|
||||
startWith,
|
||||
share,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { downloadFile, createCsv } from '../../../../utils';
|
||||
import {
|
||||
downloadFile,
|
||||
createCsv,
|
||||
arrayAttribute,
|
||||
ArrayAttributeTransform,
|
||||
} from '../../../../utils';
|
||||
import { ContentLoadingComponent } from '../../../content-loading';
|
||||
import { ProgressModule } from '../../../progress';
|
||||
import { ValueComponent, ValueListComponent } from '../../../value';
|
||||
import { sortDataByDefault, DEFAULT_SORT } from '../../consts';
|
||||
import { Column2, UpdateOptions, NormColumn } from '../../types';
|
||||
import { modelToSubject } from '../../utils/model-to-subject';
|
||||
import { TableDataSource } from '../../utils/table-data-source';
|
||||
import { tableToCsvObject } from '../../utils/table-to-csv-object';
|
||||
import { InfinityScrollDirective } from '../infinity-scroll.directive';
|
||||
@ -56,17 +70,18 @@ import { TableInputsComponent } from '../table-inputs.component';
|
||||
import { TableProgressBarComponent } from '../table-progress-bar.component';
|
||||
|
||||
import { COLUMN_DEFS } from './consts';
|
||||
import { TreeData, TreeInlineData } from './tree-data';
|
||||
import { filterSort, columnsDataToFilterSearchData } from './utils/filter-sort';
|
||||
import { toObservableColumnsData, toColumnsData } from './utils/to-columns-data';
|
||||
import { TreeData } from './tree-data';
|
||||
import { columnsDataToFilterSearchData, filterData, sortData } from './utils/filter-sort';
|
||||
import {
|
||||
toObservableColumnsData,
|
||||
toColumnsData,
|
||||
DisplayedDataItem,
|
||||
DisplayedData,
|
||||
} from './utils/to-columns-data';
|
||||
|
||||
export const TABLE_WRAPPER_STYLE = `
|
||||
display: block;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
height: 100%;
|
||||
`;
|
||||
const SHORT_DEBOUNCE_TIME_MS = 300;
|
||||
const DEBOUNCE_TIME_MS = 500;
|
||||
const DEFAULT_LOADED_LAZY_ROWS_COUNT = 3;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@ -95,12 +110,13 @@ export const TABLE_WRAPPER_STYLE = `
|
||||
TableInputsComponent,
|
||||
MatButton,
|
||||
],
|
||||
host: { style: TABLE_WRAPPER_STYLE },
|
||||
})
|
||||
export class Table2Component<T extends object, C extends object> implements OnInit {
|
||||
data = input<T[]>();
|
||||
treeData = input<TreeData<T, C>>();
|
||||
columns = input<Column2<T>[]>([]);
|
||||
columns = input<Column2<T, C>[], ArrayAttributeTransform<Column2<T, C>>>([], {
|
||||
transform: arrayAttribute,
|
||||
});
|
||||
progress = input(false, { transform: Boolean });
|
||||
hasMore = input(false, { transform: booleanAttribute });
|
||||
size = input(25, { transform: numberAttribute });
|
||||
@ -108,29 +124,48 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
noDownload = input(false, { transform: booleanAttribute });
|
||||
|
||||
// Filter
|
||||
filter = input('', { transform: (v: string | null | undefined) => (v || '').trim() });
|
||||
filterChange = output<string>();
|
||||
filter$ = modelToSubject(this.filter, this.filterChange);
|
||||
filter = model('');
|
||||
filter$ = toObservable(this.filter).pipe(
|
||||
map((v) => (v || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(DEBOUNCE_TIME_MS),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
standaloneFilter = input(false, { transform: booleanAttribute });
|
||||
displayedData$ = new BehaviorSubject<T[] | TreeInlineData<T, C>>([]);
|
||||
externalFilter = input(false, { transform: booleanAttribute });
|
||||
filteredSortData$ = new BehaviorSubject<DisplayedData<T, C> | null>(null);
|
||||
displayedData$ = combineLatest([
|
||||
defer(() => this.dataSource.data$),
|
||||
this.filteredSortData$.pipe(distinctUntilChanged()),
|
||||
defer(() => this.columnsDataProgress$),
|
||||
]).pipe(
|
||||
map(
|
||||
([data, filteredSortData, columnsDataProgress]) =>
|
||||
(filteredSortData && !columnsDataProgress ? filteredSortData : data) || [],
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
displayedCount$ = this.displayedData$.pipe(
|
||||
map((data) => data.length),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
// Select
|
||||
rowSelectable = input(false, { transform: booleanAttribute });
|
||||
rowSelected = input<T[] | TreeInlineData<T, C>>([]);
|
||||
rowSelectedChange = output<T[] | TreeInlineData<T, C>>();
|
||||
selected$ = modelToSubject(this.rowSelected, this.rowSelectedChange);
|
||||
rowSelected = model<DisplayedData<T, C>>([]);
|
||||
|
||||
// Sort
|
||||
sort = input<Sort>(DEFAULT_SORT);
|
||||
sortChange = output<Sort>();
|
||||
sort$ = modelToSubject(this.sort, this.sortChange);
|
||||
sort = model<Sort>(DEFAULT_SORT);
|
||||
@ViewChild(MatSort) sortComponent!: MatSort;
|
||||
|
||||
update = output<UpdateOptions>();
|
||||
more = output<UpdateOptions>();
|
||||
|
||||
loadedLazyItems = new WeakMap<DisplayedDataItem<T, C>, boolean>();
|
||||
|
||||
dataSource = new TableDataSource<T, C>();
|
||||
normColumns = computed<NormColumn<T>[]>(() => this.columns().map((c) => new NormColumn(c)));
|
||||
normColumns = computed<NormColumn<T, C>[]>(() => this.columns().map((c) => new NormColumn(c)));
|
||||
displayedNormColumns$ = toObservable(this.normColumns).pipe(
|
||||
switchMap((cols) =>
|
||||
combineLatest(cols.map((c) => c.hidden)).pipe(
|
||||
@ -157,30 +192,21 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
);
|
||||
isPreload = signal(false);
|
||||
loadSize = computed(() => (this.isPreload() ? this.maxSize() : this.size()));
|
||||
count$ = combineLatest([
|
||||
this.filter$,
|
||||
this.displayedData$,
|
||||
this.dataSource.data$,
|
||||
this.columnsDataProgress$,
|
||||
]).pipe(
|
||||
map(([filter, filtered, source, columnsDataProgress]) =>
|
||||
filter && !columnsDataProgress ? filtered?.length : source?.length,
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
hasAutoShowMore$ = combineLatest([
|
||||
toObservable(this.hasMore),
|
||||
this.dataSource.data$,
|
||||
this.displayedData$,
|
||||
this.dataSource.data$.pipe(map((d) => d?.length)),
|
||||
this.displayedCount$,
|
||||
this.dataSource.paginator.page.pipe(
|
||||
startWith(null),
|
||||
map(() => this.dataSource.paginator.pageSize),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([hasMore, data, filteredData, size]) =>
|
||||
(hasMore && filteredData.length === data.length) || filteredData.length > size,
|
||||
([hasMore, dataCount, displayedDataCount, size]) =>
|
||||
(hasMore && displayedDataCount !== 0 && displayedDataCount >= dataCount) ||
|
||||
displayedDataCount > size,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@ -202,9 +228,15 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
constructor(
|
||||
private dr: DestroyRef,
|
||||
private injector: Injector,
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const sort$ = toObservable(this.sort, { injector: this.injector }).pipe(
|
||||
distinctUntilChanged(),
|
||||
share(),
|
||||
);
|
||||
|
||||
toObservable(this.data, { injector: this.injector })
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.dr))
|
||||
.subscribe((data) => {
|
||||
@ -215,43 +247,47 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
.subscribe((data) => {
|
||||
this.dataSource.setTreeData(data);
|
||||
});
|
||||
const filter$ = this.filter$.pipe(
|
||||
map((filter) => filter?.trim?.() ?? ''),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(500),
|
||||
share(),
|
||||
);
|
||||
toObservable(this.filter, { injector: this.injector })
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe((filter) => {
|
||||
this.filter$.next(filter);
|
||||
});
|
||||
filter$.pipe(takeUntilDestroyed(this.dr)).subscribe((filter) => {
|
||||
this.filterChange.emit(filter);
|
||||
});
|
||||
combineLatest([
|
||||
filter$,
|
||||
this.sort$,
|
||||
this.filter$,
|
||||
sort$,
|
||||
this.dataSource.data$,
|
||||
runInInjectionContext(this.injector, () =>
|
||||
this.columnsData$.pipe(columnsDataToFilterSearchData),
|
||||
),
|
||||
this.dataSource.isTreeData$,
|
||||
toObservable(this.normColumns, { injector: this.injector }),
|
||||
toObservable(this.externalFilter, { injector: this.injector }),
|
||||
])
|
||||
.pipe(
|
||||
map(([search, sort, source, data, isTreeData, columns]) =>
|
||||
filterSort({ search, sort, source, data, isTreeData, columns }),
|
||||
),
|
||||
tap(() => {
|
||||
this.filteredSortData$.next(null);
|
||||
}),
|
||||
debounceTime(SHORT_DEBOUNCE_TIME_MS),
|
||||
map(([search, sort, source, data, isTreeData, columns, isExternalFilter]) => {
|
||||
if (isTreeData) {
|
||||
return source;
|
||||
}
|
||||
const filteredData =
|
||||
!isExternalFilter && search ? filterData(data, search) : source;
|
||||
return sortData(filteredData, data, columns, sort);
|
||||
}),
|
||||
// distinctUntilChanged(isEqual),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe((filtered) => {
|
||||
this.updateSortFilter(filtered);
|
||||
this.updateLoadedLazyItems(filtered);
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
filter$.pipe(filter(Boolean), takeUntilDestroyed(this.dr)).subscribe(() => {
|
||||
this.sortChange.emit(DEFAULT_SORT);
|
||||
});
|
||||
merge(filter$, this.sort$)
|
||||
merge(
|
||||
this.filter$.pipe(filter(Boolean)),
|
||||
toObservable(this.hasMore, { injector: this.injector }).pipe(filter(Boolean)),
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.sort.set(DEFAULT_SORT);
|
||||
});
|
||||
merge(this.filter$, sort$)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.reset();
|
||||
@ -319,8 +355,8 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private updateSortFilter(filtered: TreeInlineData<T, C> | T[]) {
|
||||
this.displayedData$.next(filtered);
|
||||
private updateSortFilter(filtered: DisplayedData<T, C>) {
|
||||
this.filteredSortData$.next(filtered);
|
||||
this.dataSource.sortData = filtered ? () => filtered : sortDataByDefault;
|
||||
this.dataSource.sort = this.sortComponent;
|
||||
}
|
||||
@ -336,4 +372,11 @@ export class Table2Component<T extends object, C extends object> implements OnIn
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.dataSource.data = this.dataSource.data;
|
||||
}
|
||||
|
||||
private updateLoadedLazyItems(items: DisplayedData<T, C>) {
|
||||
const lazyLoadedItems = items.slice(0, DEFAULT_LOADED_LAZY_ROWS_COUNT);
|
||||
for (const item of lazyLoadedItems) {
|
||||
this.loadedLazyItems.set(item, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ function getWeight(
|
||||
return 0;
|
||||
}
|
||||
|
||||
function filterData<T extends object, C extends object>(
|
||||
export function filterData<T extends object, C extends object>(
|
||||
data: FilterSearchData<T, C>,
|
||||
search: string,
|
||||
) {
|
||||
@ -80,7 +80,7 @@ function filterData<T extends object, C extends object>(
|
||||
.map((v) => v.value);
|
||||
}
|
||||
|
||||
function sortData<T extends object, C extends object>(
|
||||
export function sortData<T extends object, C extends object>(
|
||||
source: DisplayedData<T, C>,
|
||||
data: FilterSearchData<T, C>,
|
||||
columns: NormColumn<T, C>[],
|
||||
@ -90,33 +90,13 @@ function sortData<T extends object, C extends object>(
|
||||
return source;
|
||||
}
|
||||
const colIdx = columns.findIndex((c) => c.field === sort.active);
|
||||
const sortedData = source.sort((a, b) =>
|
||||
compareDifferentTypes(
|
||||
(data.get(a)?.byColumns ?? [])[colIdx]?.[0],
|
||||
(data.get(b)?.byColumns ?? [])[colIdx]?.[0],
|
||||
),
|
||||
);
|
||||
const sortedData = source
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
compareDifferentTypes(
|
||||
(data.get(a)?.byColumns ?? [])[colIdx]?.[0],
|
||||
(data.get(b)?.byColumns ?? [])[colIdx]?.[0],
|
||||
),
|
||||
);
|
||||
return sort.direction === 'desc' ? sortedData.reverse() : sortedData;
|
||||
}
|
||||
|
||||
export function filterSort<T extends object, C extends object>({
|
||||
search,
|
||||
sort,
|
||||
source,
|
||||
data,
|
||||
isTreeData,
|
||||
columns,
|
||||
}: {
|
||||
search: string;
|
||||
sort: Sort;
|
||||
source: DisplayedData<T, C>;
|
||||
data: FilterSearchData<T, C>;
|
||||
isTreeData: boolean;
|
||||
columns: NormColumn<T, C>[];
|
||||
}) {
|
||||
if (isTreeData) {
|
||||
return source;
|
||||
}
|
||||
const filteredData = search ? filterData(data, search) : source.slice();
|
||||
return sortData(filteredData, data, columns, sort);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { shareReplay, map } from 'rxjs/operators';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { Value } from '../../../../value';
|
||||
import { NormColumn } from '../../../types';
|
||||
import { CellFnArgs, Fn, NormColumn } from '../../../types';
|
||||
import { TreeInlineDataItem, TreeInlineData } from '../tree-data';
|
||||
|
||||
export type DisplayedDataItem<T extends object, C extends object> = TreeInlineDataItem<T, C> | T;
|
||||
@ -18,7 +18,18 @@ export type ColumnDataItem = {
|
||||
export type ColumnData = ColumnDataItem[];
|
||||
|
||||
type ScanColumnDataItem = Overwrite<ColumnDataItem, { value: Observable<Value | null> }>;
|
||||
type ScanColumnData = ScanColumnDataItem[];
|
||||
type ScanColumnData<T extends object, C extends object> = Map<NormColumn<T, C>, ScanColumnDataItem>;
|
||||
type ColumnDataMap<T extends object, C extends object> = Map<
|
||||
DisplayedDataItem<T, C>,
|
||||
ScanColumnData<T, C>
|
||||
>;
|
||||
|
||||
function getValue<T extends object>(
|
||||
cellFn: Fn<Observable<Value>, CellFnArgs<T>> | undefined,
|
||||
...[value, idx]: CellFnArgs<T>
|
||||
) {
|
||||
return cellFn ? cellFn(value, idx).pipe(toScannedValue) : of(null);
|
||||
}
|
||||
|
||||
function toScannedValue(src$: Observable<Value>) {
|
||||
return src$.pipe(
|
||||
@ -30,14 +41,14 @@ function toScannedValue(src$: Observable<Value>) {
|
||||
}
|
||||
|
||||
export function toObservableColumnsData<T extends object, C extends object>(
|
||||
src$: Observable<{ isTree: boolean; data: DisplayedData<T, C>; cols: NormColumn<T>[] }>,
|
||||
): Observable<Map<DisplayedDataItem<T, C>, ScanColumnData>> {
|
||||
src$: Observable<{ isTree: boolean; data: DisplayedData<T, C>; cols: NormColumn<T, C>[] }>,
|
||||
): Observable<ColumnDataMap<T, C>> {
|
||||
return src$.pipe(
|
||||
scan(
|
||||
(acc, { isTree, data, cols }) => {
|
||||
const isColsNotChanged = acc.cols === cols;
|
||||
return {
|
||||
res: new Map<TreeInlineDataItem<T, C> | T, ScanColumnData>(
|
||||
res: new Map<TreeInlineDataItem<T, C> | T, ScanColumnData<T, C>>(
|
||||
isTree
|
||||
? (data as TreeInlineData<T, C>).map((d, idx) => [
|
||||
d,
|
||||
@ -45,26 +56,41 @@ export function toObservableColumnsData<T extends object, C extends object>(
|
||||
d === acc.data[idx] &&
|
||||
// This is not the last value, because we need to calculate isNextChild
|
||||
idx !== acc.data.length - 1
|
||||
? (acc.res.get(d) as ScanColumnData)
|
||||
: cols.map((c) => ({
|
||||
value: (d.child && c.child
|
||||
? c.child(d.child, idx)
|
||||
: d.value
|
||||
? c.cell(d.value, idx)
|
||||
: of<Value>({ value: '' })
|
||||
).pipe(toScannedValue),
|
||||
isChild: !d.value,
|
||||
isNextChild: !(data as TreeInlineData<T, C>)[idx + 1]
|
||||
?.value,
|
||||
})),
|
||||
? (acc.res.get(d) as ScanColumnData<T, C>)
|
||||
: new Map(
|
||||
cols.map((c) => [
|
||||
c,
|
||||
{
|
||||
value:
|
||||
d.child && c.child
|
||||
? getValue(c.child, d.child, idx)
|
||||
: d.value
|
||||
? getValue(c.cell, d.value, idx)
|
||||
: of({ value: '' }),
|
||||
// TODO add support of lazyValue
|
||||
isChild: !d.value,
|
||||
isNextChild: !(data as TreeInlineData<T, C>)[
|
||||
idx + 1
|
||||
]?.value,
|
||||
},
|
||||
]),
|
||||
),
|
||||
])
|
||||
: (data as T[]).map((d, idx) => [
|
||||
d,
|
||||
isColsNotChanged && d === acc.data[idx]
|
||||
? (acc.res.get(d) as ScanColumnData)
|
||||
: cols.map((c) => ({
|
||||
value: c.cell(d, idx).pipe(toScannedValue),
|
||||
})),
|
||||
? (acc.res.get(d) as ScanColumnData<T, C>)
|
||||
: new Map(
|
||||
cols.map((c) => [
|
||||
c,
|
||||
{
|
||||
value: getValue(c.cell, d, idx),
|
||||
lazyValue: c.lazyCell
|
||||
? c.lazyCell(d, idx).pipe(toScannedValue)
|
||||
: undefined,
|
||||
},
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
data,
|
||||
@ -73,8 +99,8 @@ export function toObservableColumnsData<T extends object, C extends object>(
|
||||
},
|
||||
{ data: [], cols: [], res: new Map() } as {
|
||||
data: DisplayedData<T, C>;
|
||||
cols: NormColumn<T>[];
|
||||
res: Map<DisplayedDataItem<T, C>, ScanColumnData>;
|
||||
cols: NormColumn<T, C>[];
|
||||
res: ColumnDataMap<T, C>;
|
||||
},
|
||||
),
|
||||
map(({ res }) => res),
|
||||
@ -82,7 +108,7 @@ export function toObservableColumnsData<T extends object, C extends object>(
|
||||
}
|
||||
|
||||
export function toColumnsData<T extends object, C extends object>(
|
||||
src$: Observable<Map<DisplayedDataItem<T, C>, ScanColumnData>>,
|
||||
src$: Observable<ColumnDataMap<T, C>>,
|
||||
): Observable<Map<DisplayedDataItem<T, C>, ColumnData>> {
|
||||
return src$.pipe(
|
||||
switchMap((columnsData) => {
|
||||
@ -91,7 +117,11 @@ export function toColumnsData<T extends object, C extends object>(
|
||||
}
|
||||
return combineLatest(
|
||||
Array.from(columnsData.values()).map((v) =>
|
||||
combineLatest(v.map((cell) => timer(0).pipe(switchMap(() => cell.value)))),
|
||||
combineLatest(
|
||||
Array.from(v.values()).map((cell) =>
|
||||
timer(0).pipe(switchMap(() => cell.value)),
|
||||
),
|
||||
),
|
||||
),
|
||||
).pipe(
|
||||
map(
|
||||
@ -99,7 +129,10 @@ export function toColumnsData<T extends object, C extends object>(
|
||||
new Map(
|
||||
Array.from(columnsData.entries()).map(([k, v], idx) => [
|
||||
k,
|
||||
v.map((cell, colIdx) => ({ ...cell, value: res[idx][colIdx] })),
|
||||
Array.from(v.values()).map((cell, colIdx) => ({
|
||||
...cell,
|
||||
value: res[idx][colIdx],
|
||||
})),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
@ -4,11 +4,11 @@ import startCase from 'lodash-es/startCase';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { PossiblyAsync, getPossiblyAsyncObservable } from '../../../utils';
|
||||
import { Async, PossiblyAsync, getPossiblyAsyncObservable } from '../../../utils';
|
||||
import { Value } from '../../value';
|
||||
|
||||
type Fn<R, P extends Array<unknown> = []> = (...args: P) => R;
|
||||
type PossiblyFn<R, P extends Array<unknown> = []> = Fn<R, P> | R;
|
||||
export type Fn<R, P extends Array<unknown> = []> = (...args: P) => R;
|
||||
export type PossiblyFn<R, P extends Array<unknown> = []> = Fn<R, P> | R;
|
||||
|
||||
export type CellFnArgs<T extends object> = [data: T, index: number];
|
||||
|
||||
@ -21,9 +21,13 @@ export type CellValue = 'string' | Value;
|
||||
|
||||
export interface Column2<T extends object, C extends object = object> extends ColumnParams {
|
||||
field: string;
|
||||
|
||||
header?: PossiblyAsync<Partial<Value> | string>;
|
||||
|
||||
cell?: PossiblyFn<PossiblyAsync<CellValue>, CellFnArgs<T>>;
|
||||
lazyCell?: PossiblyFn<Async<CellValue>, CellFnArgs<T>>;
|
||||
child?: PossiblyFn<PossiblyAsync<CellValue>, CellFnArgs<C>>;
|
||||
|
||||
hidden?: PossiblyAsync<boolean>;
|
||||
sort?: PossiblyAsync<boolean>;
|
||||
}
|
||||
@ -53,14 +57,15 @@ export function normalizeCell<T extends object, C extends object>(
|
||||
export class NormColumn<T extends object, C extends object = object> {
|
||||
field!: string;
|
||||
header!: Observable<Value>;
|
||||
cell!: Fn<Observable<Value>, CellFnArgs<T>>;
|
||||
cell?: Fn<Observable<Value>, CellFnArgs<T>>;
|
||||
lazyCell?: Fn<Observable<Value>, CellFnArgs<T>>;
|
||||
child?: Fn<Observable<Value>, CellFnArgs<C>>;
|
||||
hidden!: Observable<boolean>;
|
||||
sort!: Observable<boolean | null>;
|
||||
params!: ColumnParams;
|
||||
|
||||
constructor(
|
||||
{ field, header, cell, child, hidden, sort, ...params }: Column2<T, C>,
|
||||
{ field, header, cell, child, hidden, sort, lazyCell, ...params }: Column2<T, C>,
|
||||
commonParams: ColumnParams = {},
|
||||
) {
|
||||
this.field = field ?? (typeof header === 'string' ? header : Math.random());
|
||||
@ -72,7 +77,12 @@ export class NormColumn<T extends object, C extends object = object> {
|
||||
: { value: value ?? defaultHeaderValue },
|
||||
),
|
||||
);
|
||||
this.cell = normalizeCell(this.field, cell, !!child);
|
||||
if (cell || !lazyCell) {
|
||||
this.cell = normalizeCell(this.field, cell, !!child);
|
||||
}
|
||||
if (lazyCell) {
|
||||
this.lazyCell = normalizeCell(this.field, lazyCell, !!child);
|
||||
}
|
||||
if (child) {
|
||||
this.child = normalizeCell(this.field, child);
|
||||
}
|
||||
|
@ -10,19 +10,20 @@ import { createUniqueColumnDef } from './create-unique-column-def';
|
||||
|
||||
export function createColumn<P, A extends object>(
|
||||
createCell: (cellParams: P, ...args: CellFnArgs<A>) => PossiblyAsync<Value>,
|
||||
columnObject: Partial<Omit<Column2<object>, 'cell'>> = {},
|
||||
columnObject: Partial<Column2<A>> = {},
|
||||
) {
|
||||
return <T extends A>(
|
||||
getCellParams: (...args: CellFnArgs<T>) => PossiblyAsync<P>,
|
||||
column: Partial<Column2<T>> = {},
|
||||
{ isLazyCell, ...column }: Partial<Column2<T>> & { isLazyCell?: boolean } = {},
|
||||
): Column2<T> => {
|
||||
const injector = inject(Injector);
|
||||
const field = column?.field ?? createUniqueColumnDef(column?.header);
|
||||
const cellKey = isLazyCell ? 'lazyCell' : 'cell';
|
||||
return {
|
||||
field,
|
||||
...columnObject,
|
||||
...column,
|
||||
cell: (...cellArgs) => {
|
||||
[cellKey]: (...cellArgs: CellFnArgs<T>) => {
|
||||
const cellValue$ = getPossiblyAsyncObservable(getCellParams(...cellArgs)).pipe(
|
||||
switchMap((cellParams) =>
|
||||
getPossiblyAsyncObservable(
|
||||
@ -32,10 +33,10 @@ export function createColumn<P, A extends object>(
|
||||
),
|
||||
),
|
||||
);
|
||||
if (column.cell) {
|
||||
if (column[cellKey]) {
|
||||
const columnCellValue$ = normalizeCell(
|
||||
field,
|
||||
column.cell,
|
||||
column[cellKey],
|
||||
!!column?.child,
|
||||
)(...cellArgs);
|
||||
return combineLatest([cellValue$, columnCellValue$]).pipe(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export function createIntersectionObserver(el: HTMLElement) {
|
||||
return new Observable((subscriber) => {
|
||||
return new Observable<IntersectionObserverEntry>((subscriber) => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
|
@ -1,25 +0,0 @@
|
||||
import {
|
||||
inject,
|
||||
DestroyRef,
|
||||
OutputEmitterRef,
|
||||
Injector,
|
||||
InputSignalWithTransform,
|
||||
} from '@angular/core';
|
||||
import { toObservable, outputToObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ReplaySubject, merge } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
export function modelToSubject<T, TransformT>(
|
||||
input: InputSignalWithTransform<T, TransformT>,
|
||||
output: OutputEmitterRef<T>,
|
||||
): ReplaySubject<T> {
|
||||
const sub$ = new ReplaySubject<T>(1);
|
||||
const dr = inject(DestroyRef);
|
||||
const injector = inject(Injector);
|
||||
merge(toObservable(input, { injector }), outputToObservable(output))
|
||||
.pipe(distinctUntilChanged(), takeUntilDestroyed(dr))
|
||||
.subscribe((v) => {
|
||||
sub$.next(v as T);
|
||||
});
|
||||
return sub$;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { TemplateRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Color } from '../../../styles';
|
||||
|
||||
@ -10,7 +11,7 @@ export type BaseValue<V = unknown> = {
|
||||
color?: Color;
|
||||
|
||||
click?: (event: MouseEvent) => void;
|
||||
link?: (event: MouseEvent) => void;
|
||||
link?: (event: MouseEvent) => string | Parameters<Router['navigate']>;
|
||||
|
||||
template?: TemplateRef<unknown>;
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export function createInputSubject<T extends Record<P, V>, P extends string, V>(
|
||||
target: T,
|
||||
propertyKey: P,
|
||||
_initValue: V,
|
||||
): BehaviorSubject<V> {
|
||||
const sub$ = new BehaviorSubject<unknown>(target[propertyKey]);
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get: function () {
|
||||
return sub$.value;
|
||||
},
|
||||
set: function (v) {
|
||||
sub$.next(v);
|
||||
},
|
||||
});
|
||||
return sub$ as never;
|
||||
}
|
@ -1,66 +1,67 @@
|
||||
@if (resValue$ | async; as v) {
|
||||
@if (v.template) {
|
||||
<ng-container *ngTemplateOutlet="v.template; context: {}"></ng-container>
|
||||
} @else {
|
||||
@if (v.color) {
|
||||
<v-tag [color]="v.color">
|
||||
<ng-container *ngTemplateOutlet="tooltipTemplate"></ng-container>
|
||||
</v-tag>
|
||||
@if (!value() && lazyValue() && !lazyVisible()) {
|
||||
<button class="button" mat-icon-button (click)="lazyVisible.set(true)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
@if (value$ | async; as v) {
|
||||
@if (v.template) {
|
||||
<ng-container *ngTemplateOutlet="v.template; context: {}"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="tooltipTemplate"></ng-container>
|
||||
}
|
||||
|
||||
<ng-template #tooltipTemplate>
|
||||
@if (v.tooltip) {
|
||||
<span [matTooltip]="v.tooltip ?? ''" class="tooltip">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</span>
|
||||
@if (v.color) {
|
||||
<v-tag [color]="v.color">
|
||||
<ng-container *ngTemplateOutlet="tooltipTemplate"></ng-container>
|
||||
</v-tag>
|
||||
} @else {
|
||||
<span [title]="v.value">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</span>
|
||||
<ng-container *ngTemplateOutlet="tooltipTemplate"></ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #valueTemplate>
|
||||
@switch (v.type) {
|
||||
@case ('menu') {
|
||||
<v-menu-value [value]="v"></v-menu-value>
|
||||
<ng-template #tooltipTemplate>
|
||||
@if (v.tooltip) {
|
||||
<span [matTooltip]="v.tooltip ?? ''" class="tooltip">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</span>
|
||||
} @else {
|
||||
<span [title]="v.value">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</span>
|
||||
}
|
||||
@default {
|
||||
@let renderedValue = renderedValue$ | async;
|
||||
@if (inProgress$ | async) {
|
||||
<v-content-loading [hiddenText]="renderedValue"></v-content-loading>
|
||||
} @else if (renderedValue) {
|
||||
<span
|
||||
[ngClass]="{ value__click: v.click, value__link: v.link }"
|
||||
[vHighlightSearch]="highlight"
|
||||
[vHighlightText]="renderedValue"
|
||||
vHighlight
|
||||
(click)="click($event)"
|
||||
></span>
|
||||
} @else if (emptySymbol) {
|
||||
<span style="color: #ccc">{{ emptySymbol }}</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #valueTemplate>
|
||||
@switch (v.type) {
|
||||
@case ('menu') {
|
||||
<v-menu-value [value]="v"></v-menu-value>
|
||||
}
|
||||
@default {
|
||||
@let valueText = valueText$ | async;
|
||||
@if (inProgress$ | async) {
|
||||
<v-content-loading [hiddenText]="valueText"></v-content-loading>
|
||||
} @else if (valueText) {
|
||||
<span
|
||||
[ngClass]="{ value__click: v.click, value__link: v.link }"
|
||||
[vHighlightSearch]="highlight"
|
||||
[vHighlightText]="valueText"
|
||||
vHighlight
|
||||
(click)="click($event)"
|
||||
></span>
|
||||
} @else if (emptySymbol) {
|
||||
<span style="color: #ccc">{{ emptySymbol }}</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
@if (v.description) {
|
||||
<div
|
||||
[title]="v.description"
|
||||
[vHighlightSearch]="highlight"
|
||||
[vHighlightText]="v.description"
|
||||
class="description mat-caption mat-secondary-text"
|
||||
vHighlight
|
||||
></div>
|
||||
@if (v.description) {
|
||||
<div
|
||||
[title]="v.description"
|
||||
[vHighlightSearch]="highlight"
|
||||
[vHighlightText]="v.description"
|
||||
class="description mat-caption mat-secondary-text"
|
||||
vHighlight
|
||||
></div>
|
||||
}
|
||||
} @else {
|
||||
<v-content-loading></v-content-loading>
|
||||
}
|
||||
<!-- } @else {-->
|
||||
<!-- <button class="button" mat-icon-button (click)="this.toggleLazyVisible()">-->
|
||||
<!-- <mat-icon>sync</mat-icon>-->
|
||||
<!-- </button>-->
|
||||
<!-- }-->
|
||||
} @else {
|
||||
<v-content-loading></v-content-loading>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
:host {
|
||||
cursor: default;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
::ng-deep .inline {
|
||||
|
@ -6,24 +6,24 @@ import {
|
||||
Injector,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
input,
|
||||
model,
|
||||
} from '@angular/core';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { combineLatest, switchMap, of, isObservable, BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, shareReplay, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { combineLatest, switchMap, of, isObservable, Observable } from 'rxjs';
|
||||
import { map, shareReplay, first, filter } from 'rxjs/operators';
|
||||
|
||||
import { HighlightDirective } from '../../directives';
|
||||
import { Nil } from '../../utils';
|
||||
import { ContentLoadingComponent } from '../content-loading';
|
||||
import { TagModule } from '../tag';
|
||||
|
||||
import { MenuValueComponent } from './components/menu-value.component';
|
||||
import { Value } from './types/value';
|
||||
import { createInputSubject } from './utils/create-input-subject';
|
||||
import { valueToString } from './utils/value-to-string';
|
||||
|
||||
@Component({
|
||||
@ -47,11 +47,9 @@ import { valueToString } from './utils/value-to-string';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ValueComponent {
|
||||
@Input() value: Value | null | undefined = null;
|
||||
value$ = createInputSubject(this, 'value', this.value);
|
||||
|
||||
@Input() lazyValue: Observable<Value> | undefined = undefined;
|
||||
lazyValue$ = createInputSubject(this, 'lazyValue', this.lazyValue);
|
||||
value = input<Value | Nil>();
|
||||
lazyValue = input<Observable<Value> | Nil>();
|
||||
lazyVisible = model<boolean>(false);
|
||||
|
||||
progress = input(false, { transform: booleanAttribute });
|
||||
@Input({ transform: booleanAttribute }) inline = false;
|
||||
@ -62,50 +60,45 @@ export class ValueComponent {
|
||||
|
||||
@Input() highlight?: string | null;
|
||||
|
||||
@Output() lazyVisibleChange = new EventEmitter<boolean>();
|
||||
|
||||
resultingValue$ = of(null);
|
||||
|
||||
lazyVisible = new BehaviorSubject(false);
|
||||
isLoaded$ = combineLatest([of(null), this.lazyVisible]).pipe(
|
||||
map(([lazyValue, lazyVisible]) => !lazyValue || lazyVisible),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
resValue$ = combineLatest([this.value$, this.lazyValue$]).pipe(
|
||||
value$ = combineLatest([toObservable(this.value), toObservable(this.lazyValue)]).pipe(
|
||||
switchMap(([value, lazyValue]) => (isObservable(lazyValue) ? lazyValue : of(value))),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
renderedValue$ = combineLatest([this.resultingValue$, this.resValue$]).pipe(
|
||||
map(
|
||||
([resultingValue, resValue]) =>
|
||||
resultingValue ??
|
||||
runInInjectionContext(this.injector, () => valueToString(resValue)),
|
||||
),
|
||||
valueText$ = this.value$.pipe(
|
||||
map((value) => runInInjectionContext(this.injector, () => valueToString(value))),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
inProgress$ = combineLatest([
|
||||
toObservable(this.progress),
|
||||
this.resValue$.pipe(map((v) => v?.inProgress ?? false)),
|
||||
this.value$.pipe(map((v) => v?.inProgress ?? false)),
|
||||
]).pipe(
|
||||
map(([compProgress, valueProgress]) => compProgress || valueProgress),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
constructor(
|
||||
private injector: Injector,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
toggleLazyVisible() {
|
||||
this.lazyVisible.next(true);
|
||||
this.lazyVisibleChange.emit(true);
|
||||
makeLazyVisible() {
|
||||
this.lazyVisible.set(true);
|
||||
}
|
||||
|
||||
click(event: MouseEvent) {
|
||||
runInInjectionContext(this.injector, () => {
|
||||
if (this.value?.click) {
|
||||
this.value.click(event);
|
||||
} else if (typeof this.value?.link === 'function') {
|
||||
this.value.link(event);
|
||||
}
|
||||
this.value$.pipe(first(), filter(Boolean)).subscribe((value) => {
|
||||
runInInjectionContext(this.injector, () => {
|
||||
if (value?.click) {
|
||||
value.click(event);
|
||||
} else if (typeof value?.link === 'function') {
|
||||
const linkArgs = value.link(event);
|
||||
if (typeof linkArgs === 'string') {
|
||||
this.router.navigateByUrl(linkArgs);
|
||||
return;
|
||||
}
|
||||
this.router.navigate(...linkArgs);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { inlineJson } from '../utils';
|
||||
name: 'inlineJson',
|
||||
})
|
||||
export class InlineJsonPipe implements PipeTransform {
|
||||
transform(value: unknown, maxReadableLever: number | false = 1): unknown {
|
||||
transform(value: unknown, maxReadableLever: number | false = 1): string {
|
||||
return inlineJson(value, maxReadableLever === false ? Infinity : maxReadableLever);
|
||||
}
|
||||
}
|
||||
|
18
projects/ng-core/src/lib/utils/decorators/debounce-time.ts
Normal file
18
projects/ng-core/src/lib/utils/decorators/debounce-time.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function DebounceTime(ms: number = 500) {
|
||||
return function (target: object, propertyKey: PropertyKey, descriptor: PropertyDescriptor) {
|
||||
let timer: number | null = null;
|
||||
const method = descriptor.value;
|
||||
|
||||
descriptor.value = function () {
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
timer = window.setTimeout(() => {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
method.apply(this, arguments);
|
||||
timer = null;
|
||||
}, ms);
|
||||
};
|
||||
};
|
||||
}
|
1
projects/ng-core/src/lib/utils/decorators/index.ts
Normal file
1
projects/ng-core/src/lib/utils/decorators/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './debounce-time';
|
@ -1,2 +1,3 @@
|
||||
export * from './is-empty';
|
||||
export * from './is-empty-primitive';
|
||||
export * from './types/nil';
|
||||
|
1
projects/ng-core/src/lib/utils/empty/types/nil.ts
Normal file
1
projects/ng-core/src/lib/utils/empty/types/nil.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Nil = null | undefined;
|
@ -1 +1,2 @@
|
||||
export * from './get-enum-keys';
|
||||
export * from './types/union-enum';
|
||||
|
2
projects/ng-core/src/lib/utils/enum/types/enum.ts
Normal file
2
projects/ng-core/src/lib/utils/enum/types/enum.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type StringEnum = string;
|
||||
export type Enum = StringEnum | number;
|
3
projects/ng-core/src/lib/utils/enum/types/union-enum.ts
Normal file
3
projects/ng-core/src/lib/utils/enum/types/union-enum.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Enum } from './enum';
|
||||
|
||||
export type UnionEnum<T extends Enum> = T | `${T}`;
|
@ -14,3 +14,5 @@ export * from './compare';
|
||||
export * from './import';
|
||||
export * from './file';
|
||||
export * from './csv';
|
||||
export * from './transform-attribute';
|
||||
export * from './decorators';
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { Nil } from '../empty';
|
||||
|
||||
export function arrayAttribute<T>(value: ArrayAttributeTransform<T>): T[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export type ArrayAttributeTransform<T> = T[] | Nil;
|
@ -0,0 +1 @@
|
||||
export * from './array-attribute';
|
Loading…
Reference in New Issue
Block a user