projects/angular/components/ui-grid/src/ui-grid.component.ts
ResizableGrid
AfterContentInit
OnChanges
OnDestroy
changeDetection | ChangeDetectionStrategy.OnPush |
encapsulation | ViewEncapsulation.None |
selector | ui-grid |
styleUrls | ./ui-grid.component.scss |
templateUrl | ./ui-grid.component.html |
collapseFiltersCount | |
Type : number
|
|
Configure if the grid search filters are eager or on open. |
collapsibleFilters | |
Type : boolean
|
|
Option to have collapsible filters. |
customFilterValue | |
Type : []
|
|
data | |
The data list that needs to be rendered within the grid. NOTE: to have access to all functionality, we recommend that entities display in the grid implement the IGridDataEntry interface. |
disabled | |
Type : boolean
|
|
Default value : false
|
|
Marks the grid enabled state. |
disableSelectionByEntry | |
Type : function
|
|
Configure a function that receives the whole grid row, and returns disabled message if the row should not be selectable |
expandedEntries | |
Set the expanded entry / entries. |
expandedEntry | |
Set the expanded entry. |
expandMode | |
Type : "preserve" | "collapse"
|
|
Default value : 'collapse'
|
|
Configure if the expanded entry should replace the active row, or add a new row with the expanded view. |
fetchStrategy | |
Configure if the grid search filters are eager or on open. |
isProjected | |
Type : boolean
|
|
Marks the grid projected state. |
isResizing | |
Type : any
|
|
Marks the grid resizing state. |
loading | |
Type : boolean
|
|
Default value : false
|
|
Marks the grid loading state. |
multiPageSelect | |
Type : boolean
|
|
Default value : false
|
|
Configure if the grid allows multi-page selection. |
noDataMessage | |
Type : string
|
|
Provide a custom |
refreshable | |
Type : boolean
|
|
Default value : true
|
|
Configure if the grid is refreshable. |
reset | |
Type : function
|
|
Default value : () => {
if (this.header) {
this.header.searchValue = '';
}
this.filterManager.clear();
this.sortManager.clear();
return of(true);
}
|
|
Clear search term, filters and sorting and emits true after. |
resizeStrategy | |
The desired resize strategy. FIXME: Currently only |
rowSize | |
Type : number
|
|
Configure the row item size for virtualScroll |
selectable | |
Type : boolean
|
|
Default value : true
|
|
Configure if the grid allows item selection. |
showHeaderRow | |
Type : boolean
|
|
Default value : true
|
|
Configure if ui-grid-header-row should be visible, by default it is visible |
showPaintTime | |
Type : boolean
|
|
Default value : false
|
|
Show paint time stats |
toggleColumns | |
Type : boolean
|
|
Default value : false
|
|
Configure if the grid allows to toggle column visibility. |
useCardView | |
Type : boolean
|
|
Default value : false
|
|
Configure if Card view should be used |
useLegacyDesign | |
Type : boolean
|
|
Option to select an alternate layout for footer pagination. |
virtualScroll | |
Type : boolean
|
|
Default value : false
|
|
Configure if |
refresh | |
Type : EventEmitter
|
|
Emits an event when user click the refresh button. |
removeCustomFilter | |
Type : EventEmitter
|
|
rendered | |
Type : EventEmitter
|
|
Emits an event once the grid has been rendered. |
resizeEnd | |
Type : EventEmitter
|
|
Emits an event once the grid has been rendered. |
rowClick | |
Type : EventEmitter
|
|
Emits an event when a row is clicked. |
sortChange | |
Type : EventEmitter
|
|
Emits an event with the sort model when a column sort changes. |
checkboxLabel | ||||||||
checkboxLabel(row?: T)
|
||||||||
Determines the
Parameters :
Returns :
string
|
checkboxTooltip | ||||||||
checkboxTooltip(row?: T)
|
||||||||
Determines the
Parameters :
Returns :
string
|
checkIndeterminateState | ||||||
checkIndeterminateState(indeterminateState: boolean)
|
||||||
Parameters :
Returns :
void
|
checkShift | ||||||
checkShift(event: Event)
|
||||||
Marks if the
Parameters :
Returns :
void
|
clearCustomFilter |
clearCustomFilter()
|
Returns :
void
|
focusRowHeader |
focusRowHeader()
|
Returns :
void
|
getColumnName | ||||||||||||
getColumnName(column: UiGridColumnDirective<T>, prefix: string)
|
||||||||||||
Parameters :
Returns :
string
|
handleSelection | ||||||||||||
handleSelection(idx: number, entry: T)
|
||||||||||||
Handles row selection, and reacts if the
Parameters :
Returns :
void
|
hideColumnHeaderTooltip | ||||||
hideColumnHeaderTooltip(tooltip: MatTooltip)
|
||||||
Parameters :
Returns :
void
|
isFilterApplied | ||||||
isFilterApplied(column: UiGridColumnDirective<T>)
|
||||||
Parameters :
Returns :
boolean
|
isRowExpanded | ||||
isRowExpanded(rowId?)
|
||||
Parameters :
Returns :
any
|
onRowClick | |||||||||
onRowClick(event: Event, row: T)
|
|||||||||
Parameters :
Returns :
void
|
searchableDropdownValue | ||||||
searchableDropdownValue(searchableDropdown: UiGridSearchFilterDirective<T>)
|
||||||
Parameters :
Returns :
ISuggestValue[]
|
toggle | ||||||
toggle(ev: MatCheckboxChange)
|
||||||
Toggles the row selection state.
Parameters :
Returns :
void
|
triggerColumnHeaderTooltip | |||||||||
triggerColumnHeaderTooltip(event: FocusOrigin, tooltip: MatTooltip)
|
|||||||||
Parameters :
Returns :
void
|
areFilersCollapsed$ |
Type : Observable<boolean>
|
columns$ |
Default value : new BehaviorSubject<UiGridColumnDirective<T>[]>([])
|
Emits the column definitions when their definition changes. |
dataManager |
Default value : new DataManager<T>(this._gridOptions)
|
Data manager, used to optimize row rendering. |
Optional displayToggleColumnsDivider$ |
Type : Observable<boolean>
|
Emits with information whether the dvider for toggle columns should be displayed |
filterManager |
Default value : new FilterManager<T>()
|
Filter manager, used to manage filter state changes. |
focusedColumnHeader |
Default value : false
|
Whether column header is focused. |
hasAnyFiltersVisible$ |
Type : Observable<boolean>
|
Emits with information whether any filter is visible. |
isAnyFilterDefined$ |
Default value : new BehaviorSubject<boolean>(false)
|
Emits with information whether filters are defined. |
Optional liveAnnouncerManager |
Type : LiveAnnouncerManager<T>
|
Live announcer manager, used to emit notification via |
resizeManager |
Type : ResizeManager<T>
|
Resize manager, used to compute resized column states. |
scrollCompensationWidth |
Type : number
|
Default value : 0
|
Returns the scroll size, in order to compensate for the scrollbar. |
selectionManager |
Default value : new SelectionManager<T>()
|
Selection manager, used to manage grid selection states. |
showFilters |
Default value : false
|
Toggle filters row display state |
sortManager |
Default value : new SortManager<T>()
|
Sort manager, used to manage sort state changes. |
visibilityManager |
Default value : new VisibilityManger<T>()
|
Visibility manager, used to manage visibility of columns. |
visible$ |
Default value : this.visibilityManager.columns$
|
Emits the visible column definitions when their definition changes. |
visibleColumnsToggle$ |
Default value : new BehaviorSubject<boolean>(false)
|
Emits when the visible columns menu has been opened or closed |
data | ||||||||
setdata(value: T[] | null)
|
||||||||
The data list that needs to be rendered within the grid. NOTE: to have access to all functionality, we recommend that entities display in the grid implement the IGridDataEntry interface.
Parameters :
Returns :
void
|
isResizing |
getisResizing()
|
Marks the grid resizing state. |
isEveryVisibleRowChecked |
getisEveryVisibleRowChecked()
|
Determines if all of the items are currently checked. |
hasValueOnVisiblePage |
gethasValueOnVisiblePage()
|
Determines if there's a value selected within the currently rendered items (used for multi-page selection). |
resizeStrategy | ||||||
setresizeStrategy(value: ResizeStrategy)
|
||||||
The desired resize strategy. FIXME: Currently only
Parameters :
Returns :
void
|
collapseFiltersCount | ||||||
getcollapseFiltersCount()
|
||||||
setcollapseFiltersCount(count: number)
|
||||||
Configure if the grid search filters are eager or on open.
Parameters :
Returns :
void
|
fetchStrategy | ||||||
getfetchStrategy()
|
||||||
setfetchStrategy(fetchStrategy: "eager" | "onOpen")
|
||||||
Configure if the grid search filters are eager or on open.
Parameters :
Returns :
void
|
collapsibleFilters | ||||||
getcollapsibleFilters()
|
||||||
setcollapsibleFilters(collapse: boolean)
|
||||||
Option to have collapsible filters.
Parameters :
Returns :
void
|
expandedEntry | ||||||
getexpandedEntry()
|
||||||
setexpandedEntry(entry: T | undefined)
|
||||||
Set the expanded entry.
Parameters :
Returns :
void
|
expandedEntries | ||||||
getexpandedEntries()
|
||||||
setexpandedEntries(entry: T | T[] | undefined)
|
||||||
Set the expanded entry / entries.
Parameters :
Returns :
void
|
customFilterValue | ||||||
setcustomFilterValue(customValue: IFilterModel<T>[])
|
||||||
Parameters :
Returns :
void
|
showMultiPageSelectionInfo |
getshowMultiPageSelectionInfo()
|
Determines if the multi-page selection row should be displayed. |
import range from 'lodash-es/range';
import {
animationFrameScheduler,
BehaviorSubject,
combineLatest,
merge,
Observable,
of,
Subject,
Subscription,
} from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
filter,
map,
observeOn,
share,
shareReplay,
startWith,
switchMap,
take,
takeUntil,
tap,
} from 'rxjs/operators';
import {
animate,
style,
transition,
trigger,
} from '@angular/animations';
import { FocusOrigin } from '@angular/cdk/a11y';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
ElementRef,
EventEmitter,
HostBinding,
Inject,
InjectionToken,
Input,
NgZone,
OnChanges,
OnDestroy,
Optional,
Output,
QueryList,
SimpleChanges,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {
MatCheckbox,
MatCheckboxChange,
} from '@angular/material/checkbox';
import { MatTooltip } from '@angular/material/tooltip';
import { QueuedAnnouncer } from '@uipath/angular/a11y';
import { ISuggestValue } from '@uipath/angular/components/ui-suggest';
import { UiGridColumnDirective } from './body/ui-grid-column.directive';
import { UiGridExpandedRowDirective } from './body/ui-grid-expanded-row.directive';
import { UiGridLoadingDirective } from './body/ui-grid-loading.directive';
import { UiGridNoContentDirective } from './body/ui-grid-no-content.directive';
import { UiGridRowActionDirective } from './body/ui-grid-row-action.directive';
import { UiGridRowCardViewDirective } from './body/ui-grid-row-card-view.directive';
import { UiGridRowConfigDirective } from './body/ui-grid-row-config.directive';
import { UiGridSearchFilterDirective } from './filters/ui-grid-search-filter.directive';
import { UiGridFooterDirective } from './footer/ui-grid-footer.directive';
import { UiGridHeaderDirective } from './header/ui-grid-header.directive';
import {
DataManager,
FilterManager,
LiveAnnouncerManager,
PerformanceMonitor,
ResizeManager,
ResizeManagerFactory,
ResizeStrategy,
SelectionManager,
SortManager,
VisibilityManger,
} from './managers';
import { ResizableGrid } from './managers/resize/types';
import {
GridOptions,
IFilterModel,
IGridDataEntry,
ISortModel,
} from './models';
import { UiGridIntl } from './ui-grid.intl';
export const UI_GRID_OPTIONS = new InjectionToken<GridOptions<unknown>>('UiGrid DataManager options.');
const DEFAULT_VIRTUAL_SCROLL_ITEM_SIZE = 48;
const FOCUSABLE_ELEMENTS_QUERY = 'a, button:not([hidden]), input:not([hidden]), textarea, select, details, [tabindex]:not([tabindex="-1"])';
@Component({
selector: 'ui-grid',
templateUrl: './ui-grid.component.html',
styleUrls: [
'./ui-grid.component.scss',
],
animations: [
trigger('filters-container', [
transition(':enter', [
style({
minHeight: '0',
height: '0',
opacity: '0',
}),
animate('0.15s ease-in', style({
opacity: '*',
minHeight: '*',
height: '*',
display: '*',
})),
]),
transition(':leave', [
style({
minHeight: '*',
height: '*',
}),
animate('0.15s ease-in', style({
opacity: '0',
minHeight: '0',
height: '0',
})),
]),
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class UiGridComponent<T extends IGridDataEntry> extends ResizableGrid<T> implements AfterContentInit, OnChanges, OnDestroy {
/**
* The data list that needs to be rendered within the grid.
*
* NOTE: to have access to all functionality, we recommend that entities display in the grid implement the IGridDataEntry interface.
*
* @param value The list that needs to rendered.
*/
@Input()
set data(value: T[] | null) {
this._performanceMonitor.reset();
this.dataManager.update(value);
}
/**
* Marks the grid resizing state.
*
*/
@HostBinding('class.ui-grid-state-resizing')
@Input()
get isResizing() {
return this.resizeManager.isResizing;
}
/**
* Marks the grid projected state.
*
*/
@HostBinding('class.ui-grid-state-projected')
@Input()
isProjected: boolean;
/**
* Determines if all of the items are currently checked.
*
*/
get isEveryVisibleRowChecked() {
return !!this.dataManager.length &&
this.dataManager.every(row => this.selectionManager.isSelected(row!));
}
/**
* Determines if there's a value selected within the currently rendered items (used for multi-page selection).
*
*/
get hasValueOnVisiblePage() {
return this.dataManager.some(row => this.selectionManager.isSelected(row!));
}
/**
* The desired resize strategy.
*
* FIXME: Currently only `ImmediateNeighbourHalt` is stable.
*
*/
@Input()
set resizeStrategy(value: ResizeStrategy) {
if (value === this._resizeStrategy) { return; }
this._resizeStrategy = value;
if (this.resizeManager != null) {
this.resizeManager.destroy();
}
this._initResizeManager();
}
/**
* Marks the grid loading state.
*
*/
@HostBinding('class.ui-grid-state-loading')
@Input()
loading = false;
/**
* Marks the grid enabled state.
*
*/
@HostBinding('class.ui-grid-state-disabled')
@Input()
disabled = false;
/**
* Configure if the grid search filters are eager or on open.
*
*/
@Input()
set collapseFiltersCount(count: number) {
if (count === this._collapseFiltersCount$.value) { return; }
this._collapseFiltersCount$.next(count);
}
get collapseFiltersCount() {
return this._collapseFiltersCount$.value;
}
/**
* Configure if the grid search filters are eager or on open.
*
*/
@Input()
set fetchStrategy(fetchStrategy: 'eager' | 'onOpen') {
if (fetchStrategy === this.fetchStrategy) { return; }
this._fetchStrategy = fetchStrategy;
}
get fetchStrategy() {
return this._fetchStrategy;
}
/**
* Configure if the grid allows item selection.
*
*/
@Input()
selectable = true;
/**
* Option to select an alternate layout for footer pagination.
*
*/
@Input()
useLegacyDesign: boolean;
/**
* Option to have collapsible filters.
*
* @deprecated - use `[collapseFiltersCount]="0" to render collapsed or leave out to always render inline`
*/
@Input()
set collapsibleFilters(collapse: boolean) {
this._collapseFiltersCount$.next(collapse ? 0 : Number.POSITIVE_INFINITY);
}
get collapsibleFilters() {
return !this._collapseFiltersCount$.value;
}
/**
* Configure if the grid allows to toggle column visibility.
*
*/
@Input()
toggleColumns = false;
/**
* Configure if the grid allows multi-page selection.
*
*/
@HostBinding('class.ui-grid-mode-multi-select')
@Input()
multiPageSelect = false;
/**
* Configure if the grid is refreshable.
*
*/
@Input()
refreshable = true;
/**
* Configure if `virtualScroll` is enabled.
*
*/
@Input()
virtualScroll = false;
/**
* Configure the row item size for virtualScroll
*
*/
@Input()
rowSize: number;
/**
* Show paint time stats
*
*/
@Input()
showPaintTime = false;
/**
* Provide a custom `noDataMessage`.
*
*/
@Input()
noDataMessage?: string;
/**
* Set the expanded entry.
*
* @deprecated Use `expandedEntries` instead.
*/
@Input()
set expandedEntry(entry: T | undefined) {
this.expandedEntries = entry;
}
get expandedEntry() {
return this._expandedEntries[0];
}
/**
* Set the expanded entry / entries.
*
*/
@Input()
set expandedEntries(entry: T | T[] | undefined) {
if (!entry) {
this._expandedEntries = [];
return;
}
this._expandedEntries = Array.isArray(entry) ? entry : [entry];
}
get expandedEntries() {
return this._expandedEntries;
}
/**
* Configure if the expanded entry should replace the active row, or add a new row with the expanded view.
*
*/
@Input()
expandMode: 'preserve' | 'collapse' = 'collapse';
/**
* Configure if ui-grid-header-row should be visible, by default it is visible
*
*/
@Input()
showHeaderRow = true;
/**
* Configure a function that receives the whole grid row, and returns
* disabled message if the row should not be selectable
*
*/
@Input()
disableSelectionByEntry: (entry: T) => null | string;
@Input()
set customFilterValue(customValue: IFilterModel<T>[]) {
if (!Array.isArray(customValue) || !customValue.length) { return; }
this.filterManager.updateCustomFilters(customValue);
}
/**
* Configure if Card view should be used
*
*/
@Input()
useCardView = false;
/**
* Emits an event with the sort model when a column sort changes.
*
*/
@Output()
sortChange = new EventEmitter<ISortModel<T>>();
/**
* Emits an event when user click the refresh button.
*
*/
@Output()
refresh = new EventEmitter<void>();
/**
* Emits an event once the grid has been rendered.
*
*/
@Output()
rendered = new EventEmitter<void>();
/**
* Emits an event once the grid has been rendered.
*
*/
@Output()
resizeEnd = new EventEmitter<void>();
@Output()
removeCustomFilter = new EventEmitter<void>();
/**
* Emits an event when a row is clicked.
*
*/
@Output()
rowClick = new EventEmitter<{ event: Event; row: T }>();
/**
* Emits the column definitions when their definition changes.
*
*/
columns$ = new BehaviorSubject<UiGridColumnDirective<T>[]>([]);
/**
* Row configuration directive reference.
*
* @ignore
*/
@ContentChild(UiGridRowConfigDirective, {
static: true,
})
rowConfig?: UiGridRowConfigDirective<T>;
/**
* Row action directive reference.
*
* @ignore
*/
@ContentChild(UiGridRowActionDirective, {
static: true,
})
actions?: UiGridRowActionDirective;
/**
* Footer directive reference.
*
* @ignore
*/
@ContentChild(UiGridFooterDirective, {
static: true,
})
footer?: UiGridFooterDirective;
/**
* Header directive reference.
*
* @ignore
*/
@ContentChild(UiGridHeaderDirective, {
static: true,
})
header?: UiGridHeaderDirective<T>;
/**
* Column directive reference list.
*
* @ignore
*/
@ContentChildren(UiGridColumnDirective)
columns!: QueryList<UiGridColumnDirective<T>>;
/**
* Expanded row template reference.
*
* @ignore
*/
@ContentChild(UiGridExpandedRowDirective, {
static: true,
})
expandedRow?: UiGridExpandedRowDirective;
/**
* No content custom template reference.
*
* @ignore
*/
@ContentChild(UiGridNoContentDirective, {
static: true,
})
noContent?: UiGridNoContentDirective;
/**
* Custom loading template reference.
*
* @ignore
*/
@ContentChild(UiGridLoadingDirective, {
static: true,
})
loadingState?: UiGridLoadingDirective;
/**
* Custom card view template reference.
*
* @ignore
*/
@ContentChild(UiGridRowCardViewDirective, {
static: true,
})
cardTemplate?: UiGridRowCardViewDirective<T>;
/**
* Reference to the grid action buttons container
*
* @ignore
*/
@ViewChild('gridActionButtons')
gridActionButtons!: ElementRef;
/**
* Reference to select all available rows checkbox
*
* @ignore
*/
@ViewChild('selectAvailableRowsCheckbox')
selectAvailableRowsCheckbox?: MatCheckbox;
/**
* Toggle filters row display state
*
*/
showFilters = false;
/**
* Live announcer manager, used to emit notification via `aria-live`.
*
*/
liveAnnouncerManager?: LiveAnnouncerManager<T>;
/**
* Selection manager, used to manage grid selection states.
*
*/
selectionManager = new SelectionManager<T>();
/**
* Data manager, used to optimize row rendering.
*
*/
dataManager = new DataManager<T>(this._gridOptions);
/**
* Filter manager, used to manage filter state changes.
*
*/
filterManager = new FilterManager<T>();
/**
* Visibility manager, used to manage visibility of columns.
*
*/
visibilityManager = new VisibilityManger<T>();
/**
* Sort manager, used to manage sort state changes.
*
*/
sortManager = new SortManager<T>();
/**
* Resize manager, used to compute resized column states.
*
*/
resizeManager!: ResizeManager<T>;
/**
* @ignore
*/
paintTime$: Observable<string>;
/**
* Emits with information whether filters are defined.
*
*/
isAnyFilterDefined$ = new BehaviorSubject<boolean>(false);
/**
* Emits with information whether any filter is visible.
*
*/
hasAnyFiltersVisible$: Observable<boolean>;
/**
* Emits with information whether the dvider for toggle columns should be displayed
*
*/
displayToggleColumnsDivider$?: Observable<boolean>;
/**
* Emits the visible column definitions when their definition changes.
*
*/
visible$ = this.visibilityManager.columns$;
/**
* Emits when the visible columns menu has been opened or closed
*
*/
visibleColumnsToggle$ = new BehaviorSubject<boolean>(false);
/**
* Returns the scroll size, in order to compensate for the scrollbar.
*
* @deprecated
*/
scrollCompensationWidth = 0;
/**
* Whether column header is focused.
*
*/
focusedColumnHeader = false;
/**
* @internal
* @ignore
*/
scrollCompensationWidth$ = this.dataManager.data$.pipe(
map(data => data.length),
distinctUntilChanged(),
observeOn(animationFrameScheduler),
debounceTime(0),
map(() => this._ref.nativeElement.querySelector('.ui-grid-viewport')),
map(view => view ? view.offsetWidth - view.clientWidth : 0),
// eslint-disable-next-line import/no-deprecated
tap(compensationWidth => this.scrollCompensationWidth = compensationWidth),
);
hasSelection$ = this.selectionManager.hasValue$.pipe(
tap(hasSelection => {
if (hasSelection && !!this.header?.actionButtons?.length) {
this._announceGridHeaderActions();
}
}),
share(),
);
renderedColumns$ = this.visible$.pipe(
map(columns => {
const firstIndex = columns.findIndex(c => c.primary);
const rowHeaderIndex = firstIndex > -1 ? firstIndex : 0;
return columns.map((directive, index) => ({
directive,
role: index === rowHeaderIndex ? 'rowheader' : 'gridcell',
}));
}),
);
areFilersCollapsed$: Observable<boolean>;
/**
* Determines if the multi-page selection row should be displayed.
*
*/
get showMultiPageSelectionInfo() {
return this.multiPageSelect &&
!this.dataManager.pristine &&
(
this.dataManager.length ||
this.selectionManager.selected.length
);
}
protected _destroyed$ = new Subject<void>();
protected _columnChanges$: Observable<SimpleChanges>;
private _fetchStrategy!: 'eager' | 'onOpen';
private _collapseFiltersCount$!: BehaviorSubject<number>;
private _resizeStrategy = ResizeStrategy.ImmediateNeighbourHalt;
private _performanceMonitor: PerformanceMonitor;
private _configure$ = new Subject<void>();
private _isShiftPressed = false;
private _lastCheckboxIdx = 0;
private _resizeSubscription$: null | Subscription = null;
private _expandedEntries: T[] = [];
/**
* @ignore
*/
constructor(
@Optional()
public intl: UiGridIntl,
protected _ref: ElementRef,
protected _cd: ChangeDetectorRef,
private _zone: NgZone,
private _queuedAnnouncer: QueuedAnnouncer,
@Inject(UI_GRID_OPTIONS)
@Optional()
private _gridOptions?: GridOptions<T>,
) {
super();
this.disableSelectionByEntry = () => null;
this.useLegacyDesign = _gridOptions?.useLegacyDesign ?? false;
this._fetchStrategy = _gridOptions?.fetchStrategy ?? 'onOpen';
this.rowSize = _gridOptions?.rowSize ?? DEFAULT_VIRTUAL_SCROLL_ITEM_SIZE;
this._collapseFiltersCount$ = new BehaviorSubject(
_gridOptions?.collapseFiltersCount ?? (_gridOptions?.collapsibleFilters === true ? 0 : Number.POSITIVE_INFINITY),
);
this.isProjected = this._ref.nativeElement.classList.contains('ui-grid-state-responsive');
this.intl = intl || new UiGridIntl();
this._columnChanges$ =
this.rendered.pipe(
switchMap(() => merge(
...this.columns.map(column =>
column.change$,
)),
),
debounceTime(10),
tap(() => this.isResizing && this.resizeManager.stop()),
);
const visibleFilterCount$ = this.rendered.pipe(
switchMap(() => this.columns.changes),
startWith('Initial emission'),
switchMap(() =>
combineLatest(this.columns.map((column: UiGridColumnDirective<T>) =>
column.dropdown?.visible$ ?? column.searchableDropdown?.visible$ ?? of(false),
)),
),
map(areVisible => areVisible.filter(visible => visible).length),
distinctUntilChanged(),
shareReplay(),
);
this.hasAnyFiltersVisible$ = visibleFilterCount$.pipe(
map(Boolean),
distinctUntilChanged(),
);
this.areFilersCollapsed$ = combineLatest([
visibleFilterCount$,
this._collapseFiltersCount$,
]).pipe(
map(([visible, minCollapse]) => visible > minCollapse),
distinctUntilChanged(),
);
const sort$ = this.sortManager
.sort$
.pipe(
tap(ev => this.sortChange.emit(ev)),
);
const inputChanges$ = merge(
this.intl.changes,
this._configure$,
this._columnChanges$,
).pipe(
map(() => this.columns.toArray()),
tap(columns => this.filterManager.columns = columns),
tap(columns => this.sortManager.columns = columns),
tap(columns => this.visibilityManager.columns = columns),
tap(columns => this.columns$.next(columns)),
tap(columns => this.isAnyFilterDefined$.next(
columns.some(c => !!c.dropdown || !!c.searchableDropdown),
)),
);
const data$ = this.dataManager.data$.pipe(
tap(_ => this._lastCheckboxIdx = 0),
);
const selection$ = this.selectionManager.changed$.pipe(
tap(_ => this._cd.markForCheck()),
);
merge(
sort$,
inputChanges$,
data$,
selection$,
).pipe(
takeUntil(this._destroyed$),
).subscribe();
this._initResizeManager();
this._performanceMonitor = new PerformanceMonitor(_ref.nativeElement);
this.paintTime$ = this._performanceMonitor.paintTime$;
this.selectionManager.hasValue$.pipe(
filter(hasValue => !hasValue && this.selectAvailableRowsCheckbox?.checked === true),
takeUntil(this._destroyed$),
).subscribe(() => this.selectAvailableRowsCheckbox!.checked = false);
this._initDisplayToggleColumnsDivider();
}
/**
* Clear search term, filters and sorting and emits true after.
*/
@Input()
reset: () => Observable<boolean> = () => {
if (this.header) {
this.header.searchValue = '';
}
this.filterManager.clear();
this.sortManager.clear();
return of(true);
};
/**
* @ignore
*/
ngAfterContentInit() {
this.selectionManager.disableSelectionByEntry = this.disableSelectionByEntry;
this.liveAnnouncerManager = new LiveAnnouncerManager(
msg => this._queuedAnnouncer.enqueue(msg),
this.intl,
this.dataManager.data$,
this.sortManager.sort$.pipe(
filter(({ userEvent }) => !!userEvent),
),
this.refresh,
this.footer?.pageChange,
);
this._configure$.next();
this._zone.onStable.pipe(
take(1),
).subscribe(() => {
// ensure everything is painted once initial rendering is done
// a lot of templates loaded lazily, this is required
// to ensure everything is drawn once the grid is initalized
this._cd.markForCheck();
this.rendered.next();
});
this.columns.changes
.pipe(
takeUntil(this._destroyed$),
).subscribe(
() => this._configure$.next(),
);
}
/**
* @ignore
*/
ngOnChanges(changes: SimpleChanges) {
const selectableChange = changes.selectable;
if (
selectableChange &&
!selectableChange.firstChange &&
selectableChange.previousValue !== selectableChange.currentValue
) {
this.selectionManager.clear();
this._configure$.next();
}
const dataChange = changes.data;
if (
dataChange &&
!dataChange.firstChange &&
!this.multiPageSelect
) {
this._performanceMonitor.reset();
this.selectionManager.clear();
}
}
/**
* @ignore
*/
ngOnDestroy() {
this.sortChange.complete();
this.rendered.complete();
this.columns$.complete();
this.isAnyFilterDefined$.complete();
this.dataManager.destroy();
this.resizeManager.destroy();
this.sortManager.destroy();
this.selectionManager.destroy();
this.filterManager.destroy();
this.visibilityManager.destroy();
if (this.liveAnnouncerManager) {
this.liveAnnouncerManager.destroy();
}
this._performanceMonitor.destroy();
this._destroyed$.next();
this._destroyed$.complete();
this._configure$.complete();
}
/**
* Marks if the `Shift` key is pressed.
*/
checkShift(event: Event) {
event.stopPropagation();
this._isShiftPressed = (event as MouseEvent | KeyboardEvent).shiftKey;
}
/**
* Handles row selection, and reacts if the `Shift` key is pressed.
*
* @param idx The clicked row index.
* @param entry The entry associated to the selected row.
*/
handleSelection(idx: number, entry: T) {
if (!this._isShiftPressed) {
this._lastCheckboxIdx = idx;
this.selectionManager.toggle(entry);
return;
}
const min = Math.min(this._lastCheckboxIdx, idx);
const max = Math.max(idx, this._lastCheckboxIdx);
const rowsForSelection = range(min, max + 1)
.map(this.dataManager.get);
const rowsForDeselection = this.dataManager.data$.getValue()
.filter(row => !rowsForSelection.find(rowForSelection => rowForSelection.id === row.id));
/**
* To be consistent with the browser, if we click on a row
* that was already selected, we unselect it, sync with DOM (detectChanges),
* then we select it again (it's included in rowsForSelection).
*/
if (this.selectionManager.isSelected(entry)) {
this.selectionManager.deselect(entry);
this._cd.detectChanges();
}
this.selectionManager.select(...rowsForSelection.filter(row => !this.selectionManager.isSelected(row)));
this.selectionManager.deselect(...rowsForDeselection.filter(row => this.selectionManager.isSelected(row)));
this._cd.detectChanges();
}
/**
* Toggles the row selection state.
*
*/
toggle(ev: MatCheckboxChange) {
if (ev.checked) {
this.dataManager.forEach(row => this.selectionManager.select(row!));
} else {
this._lastCheckboxIdx = 0;
this.dataManager.forEach(row => this.selectionManager.deselect(row!));
}
}
/**
* Determines the `checkbox` `matToolTip`.
*
* @param [row] The row for which the label is computed.
*/
checkboxTooltip(row?: T): string {
if (!row) {
return this.intl.checkboxTooltip(this.isEveryVisibleRowChecked);
}
return this.intl.checkboxTooltip(this.selectionManager.isSelected(row), this.dataManager.indexOf(row));
}
/**
* Determines the `checkbox` aria-label`.
* **DEPRECATED**
*
* @param [row] The row for which the label is computed.
*/
checkboxLabel(row?: T): string {
if (!row) {
return `${this.isEveryVisibleRowChecked ? 'select' : 'deselect'} all`;
}
return `${this.selectionManager.isSelected(row) ? 'deselect' : 'select'} row ${this.dataManager.indexOf(row)}`;
}
focusRowHeader() {
this.gridActionButtons?.nativeElement.querySelector(FOCUSABLE_ELEMENTS_QUERY)?.focus();
}
clearCustomFilter() {
this.removeCustomFilter.emit();
this.filterManager.clearCustomFilters();
}
isRowExpanded(rowId?: IGridDataEntry['id']) {
if (rowId == null) {
return false;
}
return this._expandedEntries.some(el => el.id === rowId);
}
onRowClick(event: Event, row: T) {
this.rowClick.emit({
event,
row,
});
}
checkIndeterminateState(indeterminateState: boolean) {
// If the grid has disabled rows the indeterminate can be set to false and still not have all the rows selected,
// in that case we set the indeterminate to true
if (
!indeterminateState &&
this.selectAvailableRowsCheckbox &&
this.hasValueOnVisiblePage &&
!this.isEveryVisibleRowChecked
) {
this.selectAvailableRowsCheckbox.indeterminate = true;
}
}
searchableDropdownValue(searchableDropdown: UiGridSearchFilterDirective<T>): ISuggestValue[] {
if (searchableDropdown.value) {
if (searchableDropdown.multiple) {
return searchableDropdown.value as ISuggestValue[];
}
return [searchableDropdown.value as ISuggestValue];
}
return [];
}
getColumnName(column: UiGridColumnDirective<T>, prefix = 'ui-grid-dropdown-filter') {
return prefix + '-' + ((column.property as string) ?? 'na');
}
isFilterApplied(column: UiGridColumnDirective<T>) {
return (column.dropdown?.value != null && column.dropdown!.value!.value !== column.dropdown!.emptyStateValue)
|| (column.searchableDropdown?.value != null && (column.searchableDropdown?.value as ISuggestValue[])?.length !== 0);
}
triggerColumnHeaderTooltip(event: FocusOrigin, tooltip: MatTooltip) {
if (event === 'keyboard') {
this.focusedColumnHeader = true;
tooltip.show();
}
}
hideColumnHeaderTooltip(tooltip: MatTooltip) {
tooltip.hide();
this.focusedColumnHeader = false;
}
private _announceGridHeaderActions() {
this._queuedAnnouncer.enqueue(this.intl.gridHeaderActionsNotice);
}
private _initResizeManager() {
this._resizeSubscription$?.unsubscribe();
this.resizeManager = ResizeManagerFactory(this._resizeStrategy, this);
this._resizeSubscription$ = this.resizeManager.resizeEnd$.subscribe(() => this.resizeEnd.emit());
}
private _initDisplayToggleColumnsDivider() {
this.displayToggleColumnsDivider$ = combineLatest([this.hasAnyFiltersVisible$, this.filterManager.hasCustomFilter$]).pipe(
map(([hasAnyFilterVisible, hasCustomFilters]) => hasAnyFilterVisible || hasCustomFilters),
);
}
}
<ng-container *ngIf="showPaintTime && (paintTime$ | async) as paintTime">
<div class="ui-grid-debug-information">
Painted {{ dataManager.length }} rows in {{ paintTime }}ms
</div>
</ng-container>
<div *ngIf="toggleColumns ||
header?.mainButtons?.length ||
header?.actionButtons?.length ||
header?.inlineButtons?.length ||
header?.search ||
(isAnyFilterDefined$ | async)"
class="ui-grid-filter-container">
<div class="ui-grid-filter-container-lhs-group">
<div class="ui-grid-filter-container-lhs-group-actions">
<ng-container *ngIf="filterManager.hasCustomFilter$ | async; else noCustomFilter">
<ng-container *ngTemplateOutlet="toggleColumnsTmpl"></ng-container>
<button *ngIf="!(hasSelection$ | async)"
(click)="clearCustomFilter()"
mat-flat-button
type="button"
data-cy="clear-custom-filter">
{{ intl.clearCustomFilter }}
</button>
</ng-container>
<ng-template #noCustomFilter>
<ng-container>
<ng-container *ngIf="useLegacyDesign">
<ng-container *ngTemplateOutlet="toggleColumnsTmpl"></ng-container>
</ng-container>
<ng-container *ngIf="!(hasSelection$ | async) ||
!header?.actionButtons?.length">
<ui-grid-search *ngIf="header?.search"
[debounce]="header!.searchDebounce"
[maxLength]="header!.searchMaxLength"
[placeholder]="intl.searchPlaceholder"
[searchTooltip]="intl.searchTooltip"
[clearTooltip]="intl.clearTooltip"
[tooltipDisabled]="resizeManager.isResizing"
[value]="header!.searchValue!"
(searchChange)="filterManager.searchChange($event, header!, footer)"
class="ui-grid-search ui-grid-filter-option">
</ui-grid-search>
<ng-container *ngIf="!useLegacyDesign">
<ng-container *ngTemplateOutlet="toggleColumnsTmpl"></ng-container>
</ng-container>
<ng-container *ngTemplateOutlet="filtersTmpl">
</ng-container>
</ng-container>
<div *ngIf="!(hasSelection$ | async)"
class="ui-grid-action-buttons ui-grid-action-buttons-inline">
<ng-container *ngFor="let button of header?.inlineButtons">
<ng-container *ngIf="button.visible">
<ng-container *ngTemplateOutlet="button.html ?? null">
</ng-container>
</ng-container>
</ng-container>
</div>
</ng-container>
</ng-template>
<div #gridActionButtons
*ngIf="hasSelection$ | async"
class="ui-grid-action-buttons ui-grid-action-buttons-selection">
<ng-container *ngFor="let button of header?.actionButtons">
<ng-container *ngIf="button.visible">
<ng-container *ngTemplateOutlet="button.html?? null">
</ng-container>
</ng-container>
</ng-container>
</div>
</div>
<div *ngIf="showFilters &&
(hasAnyFiltersVisible$ | async) &&
(filterManager.hasCustomFilter$ | async) === false &&
!(hasSelection$ | async)"
[@filters-container]
class="ui-grid-filter-container-lhs-group-filters">
<ng-container *ngTemplateOutlet="inlineFiltersTmpl"></ng-container>
</div>
</div>
<div class="ui-grid-filter-container-rhs-group">
<div *ngIf="!useLegacyDesign || header?.mainButtons?.length ?? 0 > 1"
class="ui-grid-action-buttons ui-grid-action-buttons-main">
<ng-container *ngFor="let button of header?.mainButtons">
<ng-container *ngIf="button.visible">
<ng-container *ngTemplateOutlet="button.html ?? null">
</ng-container>
</ng-container>
</ng-container>
</div>
</div>
</div>
<ng-container *ngIf="!useLegacyDesign && multiPageSelect then multiPageSelectionRow">
</ng-container>
<div [class.use-alternate-design]="!useLegacyDesign"
(keydown.shift.alt.arrowup)="focusRowHeader()"
class="ui-grid-container">
<div class="ui-grid-table-container">
<div *ngIf="useLegacyDesign && header?.mainButtons?.length === 1"
class="ui-grid-action-button">
<ng-container *ngIf="header!.mainButtons![0] as mainBtn">
<ng-container *ngIf="mainBtn.visible">
<ng-container *ngTemplateOutlet="mainBtn.html ?? null">
</ng-container>
</ng-container>
</ng-container>
</div>
<div [class.ui-grid-table-refreshable]="refreshable"
class="ui-grid-table"
role="grid">
<div class="ui-grid-header">
<div *ngIf="showHeaderRow"
class="ui-grid-header-row"
role="row">
<div *ngIf="selectable"
class="ui-grid-header-cell ui-grid-checkbox-cell ui-grid-feature-cell"
role="columnheader">
<mat-checkbox #selectAvailableRowsCheckbox
(change)="$event && toggle($event)"
(indeterminateChange)="checkIndeterminateState($event)"
[disabled]="!dataManager.length"
[checked]="isEveryVisibleRowChecked"
[indeterminate]="hasValueOnVisiblePage && !isEveryVisibleRowChecked"
[matTooltip]="checkboxTooltip()"
[aria-label]="checkboxTooltip()"
tabindex="0">
</mat-checkbox>
</div>
<ng-container *ngIf="multiPageSelect then multiPageSelectionProjectedHeaderCell">
</ng-container>
<div *ngFor="let column of visible$ | async; let last = last; let columnIndex = index"
[class.ui-grid-header-cell-sortable]="column.sortable"
[class.ui-grid-resizeable]="column.resizeable"
[class.ui-grid-state-resizing]="column === resizeManager.current?.column"
[attr.aria-sort]="column.ariaSort"
[attr.data-property]="column.property"
[attr.data-identifier]="column.identifier"
[style.width.%]="column.width"
(cdkFocusChange)="triggerColumnHeaderTooltip($event, columnTooltip)"
(focusout)="hideColumnHeaderTooltip(columnTooltip)"
(keyup.enter)="sortManager.changeSort(column)"
(keyup.space)="sortManager.changeSort(column)"
(keydown.ArrowLeft)="!last && resizeManager.startKeyboardResize('left', column, columnIndex)"
(keydown.ArrowRight)="!last && resizeManager.startKeyboardResize('right', column, columnIndex)"
cdkMonitorElementFocus
class="ui-grid-header-cell"
role="columnheader"
tabindex="0">
<div (click)="sortManager.changeSort(column)"
class="ui-grid-header-title">
<p #columnTooltip="matTooltip"
[class.ui-grid-header-title-filtered]="isFilterApplied(column)"
[matTooltip]="column.title + (focusedColumnHeader ? ('\n' + column.description) : '')"
[matTooltipDisabled]="resizeManager.isResizing"
[attr.aria-label]="column.title + (column.description ? ('. ' + column.description) : '') + (column.sortable && intl.sortableMessage ? '. ' + intl.sortableMessage : '')"
matTooltipClass="preserve-whitespace">
{{ column.title }}</p>
<mat-icon *ngIf="column.description"
[matTooltip]="column.description"
class="ui-grid-info-icon material-icons-outlined">info</mat-icon>
<div *ngIf="column.sortable"
[class.ui-grid-sort-indicator-asc]="column.sort === 'asc'"
[class.ui-grid-sort-indicator-desc]="column.sort === 'desc'"
class="ui-grid-sort-indicator">
<mat-icon class="ui-grid-sort-icon">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path [attr.transform]="column.sort === '' ? 'translate(0 -3)': ''"
class="path-asc"
d="M12 11.8l2.5 2.5 1.1-1.1L12 9.7l-3.6 3.5 1.1 1.1z" />
<path [attr.transform]="column.sort === '' ? 'translate(0 3)': ''"
class="path-desc"
d="M12 12.2L9.5 9.7l-1.1 1.1 3.6 3.5 3.6-3.5-1.1-1.1z" />
</svg>
</mat-icon>
</div>
</div>
<div *ngIf="column.resizeable && !last && !useCardView"
(mousedown)="resizeManager.startResize($event, column, columnIndex)"
class="ui-grid-resize-anchor">
<mat-icon>
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24">
<path
d="M11 18l-2 2-2-2 2-2 2 2zm-2-8l-2 2 2 2 2-2-2-2zm0-6L7 6l2 2 2-2-2-2zm6 4l2-2-2-2-2 2 2 2zm0 2l-2 2 2 2 2-2-2-2zm0 6l-2 2 2 2 2-2-2-2z" />
</svg>
</mat-icon>
</div>
</div>
<div *ngIf="virtualScroll"
[style.marginLeft.px]="scrollCompensationWidth$ | async"
class="ui-grid-header-cell ui-grid-scroll-size-compensation-cell ui-grid-feature-cell">
</div>
<div *ngIf="refreshable"
class="ui-grid-header-cell ui-grid-refresh-cell ui-grid-feature-cell"
role="columnheader">
<button [matTooltip]="intl.refreshTooltip"
[matTooltipDisabled]="resizeManager.isResizing"
[disabled]="disabled"
(click)="refresh.emit()"
mat-icon-button
type="button"
class="grid-refresh-button">
<mat-icon>refresh</mat-icon>
</button>
</div>
<div *ngIf="!!actions"
class="ui-grid-header-cell ui-grid-action-cell ui-grid-feature-cell">
</div>
</div>
<mat-progress-bar *ngIf="loading && !loadingState"
mode="query"
class="ui-grid-loader">
</mat-progress-bar>
</div>
<ng-container *ngIf="useLegacyDesign && multiPageSelect then multiPageSelectionRow">
</ng-container>
<ng-container *ngIf="dataManager?.length; else noData">
<ng-container *ngIf="virtualScroll">
<cdk-virtual-scroll-viewport [total]="dataManager.length"
[itemSize]="rowSize"
uiVirtualScrollViewportResize
class="ui-grid-viewport">
<ng-container *ngIf="useCardView">
<div class="ui-grid-cards-container">
<ng-container *cdkVirtualFor="let row of dataManager.data$ | async;
let last = last;
let index = index;
trackBy: dataManager.hashTrack">
<ng-container *ngTemplateOutlet="rowCardTemplate; context: {
data: row,
index: index,
expanded: expandedEntries,
last: last
}"></ng-container>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="!useCardView">
<ng-container *cdkVirtualFor="let row of dataManager.data$ | async;
let last = last;
let index = index;
trackBy: dataManager.hashTrack">
<ng-container *ngTemplateOutlet="rowTemplate; context: {
data: row,
index: index,
expanded: expandedEntries,
last: last
}">
</ng-container>
</ng-container>
</ng-container>
</cdk-virtual-scroll-viewport>
</ng-container>
<ng-container *ngIf="!virtualScroll">
<ng-container *ngIf="useCardView">
<div class="ui-grid-cards-container">
<ng-container *ngFor="let row of dataManager.data$ | async;
let index = index;
let last = last;">
<ng-container *ngTemplateOutlet="rowCardTemplate; context: {
data: row,
index: index,
expanded: expandedEntries,
last: last
}">
</ng-container>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="!useCardView">
<ng-container *ngFor="let row of dataManager.data$ | async;
let index = index;
let last = last;">
<ng-container *ngTemplateOutlet="rowTemplate; context: {
data: row,
index: index,
expanded: expandedEntries,
last: last
}">
</ng-container>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngIf="loading && !!loadingState">
<ng-container *ngTemplateOutlet="loadingState.html ?? null"></ng-container>
</ng-container>
</div>
</div>
<div class="ui-grid-footer-container">
<ng-container *ngTemplateOutlet="useLegacyDesign ? classicFooter : alternativeFooter"></ng-container>
</div>
</div>
<ng-template #rowTemplate
let-row="data"
let-expanded="expanded"
let-last="last"
let-index="index">
<div cdkMonitorSubtreeFocus
class="ui-grid-row"
*ngIf="!isRowExpanded(row?.id) || expandMode === 'preserve'"
[class.ui-grid-row-state-expanded]="isRowExpanded(row?.id)"
[class.ui-grid-border-none]="!footer && last"
[ngClass]="rowConfig?.ngClassFn(row) ?? ''"
[tabIndex]="0"
[attr.data-row-index]="index"
(click)="onRowClick($event, row)"
(keyup.enter)="onRowClick($event, row)"
role="row">
<div *ngIf="isProjected"
class="ui-grid-cell ui-grid-feature-cell ui-grid-mobile-feature-cell"
role="gridcell">
<div *ngIf="selectable"
class="ui-grid-mobile-feature-container ui-grid-mobile-refresh-container">
<mat-checkbox *ngLet="disableSelectionByEntry(row) as disabledReason"
(click)="checkShift($event)"
(keyup.shift.space)="checkShift($event)"
(keyup.space)="checkShift($event)"
(change)="handleSelection(index, row)"
[checked]="selectionManager.isSelected(row)"
[matTooltip]="disabledReason || checkboxTooltip(row)"
[aria-label]="disabledReason || checkboxTooltip(row)"
[disabled]="!!disabledReason"
tabindex="0">
</mat-checkbox>
</div>
<div *ngIf="!!actions"
role="gridcell"
class="ui-grid-mobile-feature-container ui-grid-mobile-action-container">
<ng-container *ngTemplateOutlet="actions.html ?? null; context: {
data: row,
index: index
}">
</ng-container>
</div>
</div>
<div *ngIf="!isProjected &&
selectable"
class="ui-grid-cell ui-grid-checkbox-cell ui-grid-feature-cell"
role="gridcell">
<mat-checkbox *ngLet="disableSelectionByEntry(row) as disabledReason"
(click)="checkShift($event)"
(keyup.shift.space)="checkShift($event)"
(keyup.space)="checkShift($event)"
(change)="handleSelection(index, row)"
[checked]="selectionManager.isSelected(row)"
[matTooltip]="disabledReason || checkboxTooltip(row)"
[aria-label]="disabledReason || checkboxTooltip(row)"
[disabled]="disabledReason"
tabindex="0">
</mat-checkbox>
</div>
<ng-container *ngFor="let column of renderedColumns$ | async">
<div [class.ui-grid-primary]="column.directive.primary"
[class.ui-grid-secondary]="!column.directive.primary"
[class.ui-grid-state-resizing]="column.directive === resizeManager.current?.column"
[attr.data-property]="column.directive.property"
[attr.data-identifier]="column.directive.identifier"
[style.width.%]="column.directive.width"
[attr.role]="column.role"
class="ui-grid-cell">
<div *ngIf="isProjected"
[matTooltip]="column.directive.title ?? ''"
[matTooltipDisabled]="resizeManager.isResizing"
class="ui-grid-cell-mobile-title">{{column.directive.title}}</div>
<div class="ui-grid-cell-content">
<ng-container *ngTemplateOutlet="column.directive.html || textCellTemplate; context: {
data: row,
index: index,
property: column.directive.property
}">
</ng-container>
</div>
</div>
</ng-container>
<div *ngIf="virtualScroll"
class="ui-grid-cell ui-grid-scroll-size-compensation-cell ui-grid-feature-cell">
</div>
<div *ngIf="!isProjected &&
refreshable"
class="ui-grid-cell ui-grid-refresh-cell ui-grid-feature-cell">
</div>
<div *ngIf="!isProjected &&
!!actions"
role="gridcell"
class="ui-grid-cell ui-grid-action-cell ui-grid-feature-cell">
<div class="ui-grid-action-cell-container">
<ng-container *ngTemplateOutlet="actions.html ?? null; context: {
data: row,
index: index
}">
</ng-container>
</div>
</div>
</div>
<div *ngIf="isRowExpanded(row?.id)"
[class.ui-grid-row-state-expanded]="isRowExpanded(row?.id)"
[class.ui-grid-border-none]="!footer && last"
role="row">
<div class="ui-grid-expanded-row-container">
<ng-container *ngTemplateOutlet="expandedRow!.html ?? null; context: {
data: row,
index: index
}">
</ng-container>
</div>
</div>
</ng-template>
<ng-template #textCellTemplate
let-row="data"
let-property="property">
<p [matTooltip]="dataManager.getProperty(row, property)"
[matTooltipDisabled]="resizeManager.isResizing">{{ dataManager.getProperty(row, property) }}</p>
</ng-template>
<ng-template #rowCardTemplate
let-row="data"
let-expanded="expanded"
let-last="last"
let-index="index">
<div cdkMonitorSubtreeFocus
class="ui-grid-card-wrapper"
[ngClass]="rowConfig?.ngClassFn(row) ?? ''"
[tabIndex]="0"
[attr.data-row-index]="index"
(click)="onRowClick($event, row)"
(keyup.enter)="onRowClick($event, row)"
>
<ng-container *ngIf="cardTemplate?.html; else defaultCardTemplate">
<ng-container *ngTemplateOutlet="cardTemplate?.html || defaultCardTemplate;context: {
data: row,
index: index,
expanded: expandedEntries,
last: last
}"></ng-container>
</ng-container>
</div>
</ng-template>
<ng-template #defaultCardTemplate
let-row="data"
let-index="index">
<div class="ui-grid-card-default">
<ng-container *ngFor="let column of renderedColumns$ | async">
<div [class.ui-grid-primary]="column.directive.primary"
[class.ui-grid-secondary]="!column.directive.primary"
[attr.data-property]="column.directive.property"
[attr.data-identifier]="column.directive.identifier"
[attr.role]="column.role"
class="ui-grid-card-default-cell">
<div *ngIf="isProjected"
[matTooltip]="column.directive.title ?? ''"
[matTooltipDisabled]="resizeManager.isResizing"
class="ui-grid-card-default-cell-mobile-title">{{column.directive.title}}</div>
<div class="ui-grid-card-default-cell-content">
<b>{{ column.directive.title ?? column.directive.property }}:</b> <ng-container *ngTemplateOutlet="column.directive.html || textCellTemplate; context: {
data: row,
index: index,
property: column.directive.property
}">
</ng-container>
</div>
</div>
</ng-container>
</div>
</ng-template>
<ng-template #noData>
<ng-container *ngIf="!loading">
<ng-container *ngIf="noContent; else defaultNoData">
<ng-container *ngTemplateOutlet="noContent.html ?? null; context: {
search: header?.searchValue,
activeCount: filterManager.activeCount$ | async
}">
</ng-container>
</ng-container>
</ng-container>
</ng-template>
<ng-template #defaultNoData>
<ng-container *ngIf="useLegacyDesign; else defaultNoDataAlternative">
<div *ngIf="!dataManager.pristine"
class="ui-grid-row ui-grid-no-data-container"
[class.ui-grid-border-none]="!footer">
<mat-icon>list</mat-icon>
<p>{{noDataMessage || intl.noDataMessage }}</p>
</div>
</ng-container>
</ng-template>
<ng-template #defaultNoDataAlternative>
<ng-container *ngLet="(filterManager.activeCount$ | async) as activeFilterCount">
<div *ngIf="!dataManager.pristine"
class="ui-grid-row ui-grid-no-data-container"
[class.ui-grid-no-content-available]="!header?.searchValue && !activeFilterCount"
[class.ui-grid-border-none]="!footer">
<mat-icon *ngIf="!header?.searchValue && !activeFilterCount">visibility_off</mat-icon>
<p>{{ noDataMessage || intl.noDataMessageAlternative(header?.searchValue, activeFilterCount) }}</p>
</div>
</ng-container>
</ng-template>
<ng-template #multiPageSelectionAlternate>
<div *ngIf="!isProjected &&
showMultiPageSelectionInfo"
class="ui-grid-selection-info-container ui-grid-selection-info-container-alternate">
<ng-container *ngTemplateOutlet="multiPageSelectionInfo"></ng-container>
</div>
</ng-template>
<ng-template #multiPageSelectionRow>
<div *ngIf="!isProjected &&
showMultiPageSelectionInfo"
class="ui-grid-row ui-grid-selection-info-container"
[class.ui-grid-selection-info-container-alternate]="!useLegacyDesign">
<ng-container *ngTemplateOutlet="multiPageSelectionInfo"></ng-container>
</div>
</ng-template>
<ng-template #multiPageSelectionProjectedHeaderCell>
<div *ngIf="isProjected &&
showMultiPageSelectionInfo"
class="ui-grid-header-cell ui-grid-feature-cell ui-grid-selection-info-container">
<ng-container *ngTemplateOutlet="multiPageSelectionInfo"></ng-container>
</div>
</ng-template>
<ng-template #multiPageSelectionInfo>
<ng-container *ngIf="selectionManager.selected.length; else noSelectionTmpl">
<button [matTooltip]="intl.clearSelectionTooltip"
[matTooltipDisabled]="resizeManager.isResizing"
(click)="selectionManager.clear()"
mat-icon-button
type="button"
color="warn"
class="ui-grid-selection-clear-button">
<mat-icon>clear</mat-icon>
</button>
<span class="ui-grid-selection-info-message">
{{ intl.translateMultiPageSelectionCount(selectionManager.selected.length) }}
</span>
</ng-container>
</ng-template>
<ng-template #noSelectionTmpl>
<mat-icon *ngIf="useLegacyDesign"
[matTooltip]="intl.multiPageSelectionInfoTooltip"
[matTooltipDisabled]="resizeManager.isResizing"
class="ui-grid-selection-info-icon"
tabindex="0">info</mat-icon>
<span class="ui-grid-selection-info-message">
{{ intl.noSelectionMessage }}
</span>
</ng-template>
<ng-template #classicFooter>
<mat-paginator *ngIf="!!footer && !footer.hidden"
[pageIndex]="footer.state.pageIndex"
[pageSize]="footer.state.pageSize"
[pageSizeOptions]="footer.pageSizes"
[length]="footer.length"
[disabled]="disabled"
[showFirstLastButtons]="footer.showFirstLastButtons"
[hidePageSize]="footer.hidePageSize"
(page)="footer.pageChange.next($event)">
</mat-paginator>
</ng-template>
<ng-template #alternativeFooter>
<ui-grid-custom-paginator *ngIf="!!footer && !footer.hidden"
[pageIndex]="footer.state.pageIndex"
[pageSize]="footer.state.pageSize"
[pageSizeOptions]="footer.pageSizes"
[length]="footer.length"
[disabled]="disabled"
[showFirstLastButtons]="footer.showFirstLastButtons"
[hidePageSize]="footer.hidePageSize"
[hideTotalCount]="footer.hideTotalCount"
(page)="footer.pageChange.next($event)">
</ui-grid-custom-paginator>
</ng-template>
<ng-template #filtersTmpl>
<ng-container *ngIf="areFilersCollapsed$ | async; else inlineFiltersTmpl">
<button *ngIf="hasAnyFiltersVisible$ | async"
[disabled]="disabled"
(click)="showFilters = !showFilters"
[attr.aria-expanded]="showFilters"
mat-button
type="button"
class="ui-grid-collapsible-filters-toggle">
<mat-icon>filter_list</mat-icon>
<span>{{ intl.filters(filterManager.activeCount$ | async) }}</span>
<mat-icon>{{ showFilters ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
</ng-container>
</ng-template>
<ng-template #inlineFiltersTmpl>
<ng-container *ngFor="let column of columns$ | async">
<div *ngIf="column.dropdown?.visible$ | async"
[ngClass]="{'ui-grid-alternate-filter-container': collapsibleFilters}"
class="ui-grid-dropdown-filter-container ui-grid-filter-option">
<button [matMenuTriggerFor]="filterOptions"
[disabled]="column.dropdown?.disabled || disabled"
[attr.data-column-name]="getColumnName(column)"
[expandedTranslation]="intl.menuExpanded"
uiCustomMatMenuTriggerFor
mat-button
type="button"
class="ui-grid-dropdown-filter-button">
<span class="ui-grid-dropdown-filter-title">{{ column.title }}: </span>
<span class="ui-grid-dropdown-filter-value">
{{ !!column.dropdown?.value ? intl.translateDropdownOption(column.dropdown!.value!) :
intl.noFilterPlaceholder }}
</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
<mat-menu #filterOptions="matMenu"
[overlapTrigger]="true">
<button *ngIf="column.dropdown?.showAllOption"
[class.active]="column.dropdown?.value == null"
[matTooltip]="intl.noFilterPlaceholder"
(click)="filterManager.dropdownUpdate(column!, undefined)"
type="button"
mat-menu-item>
{{ intl.noFilterPlaceholder }}
</button>
<button *ngFor="let dropdownItem of column.dropdown?.items"
[class.active]="column.dropdown?.value?.label === dropdownItem.label"
[matTooltip]="intl.translateDropdownOption(dropdownItem)"
(click)="filterManager.dropdownUpdate(column, dropdownItem)"
type="button"
mat-menu-item>
{{ intl.translateDropdownOption(dropdownItem) }}
</button>
</mat-menu>
</div>
<ng-container *ngIf="column.searchableDropdown?.visible$ | async">
<ui-suggest #suggest
[placeholder]="column.title ?? ''"
[defaultValue]="column.searchableDropdown!.noFilterPlaceholder || intl.noFilterPlaceholder"
[searchSourceFactory]="column.searchableDropdown!.searchSourceFactory"
[value]="searchableDropdownValue(column.searchableDropdown!)"
[disabled]="column.searchableDropdown!.disabled || disabled"
[drillDown]="column.searchableDropdown!.drillDown"
[maxLength]="64"
[fetchStrategy]="column.searchableDropdown!.fetchStrategy || fetchStrategy"
[attr.data-cy]="getColumnName(column, 'ui-grid-search-filter')"
[multiple]="column.searchableDropdown!.multiple"
[compact]="column.searchableDropdown!.multiple"
[displayCount]="column.searchableDropdown!.displayCount"
[minChars]="column.searchableDropdown!.minChars ?? 0"
(selected)="filterManager.searchableDropdownUpdate(column, $event, true)"
(deselected)="filterManager.searchableDropdownUpdate(column, $event, false)"
(opened)="column.refetch && suggest.fetch()"
width="230px"
class="ui-grid-search-filter ui-grid-filter-option">
</ui-suggest>
</ng-container>
</ng-container>
</ng-template>
<ng-template #toggleColumnsTmpl>
<ui-grid-toggle-columns *ngIf="toggleColumns && !(hasSelection$ | async)"
[options]="visibilityManager.options$ | async"
[dirty]="(visibilityManager.isDirty$ | async) ?? false"
[toggleTooltip]="intl.toggleTooltip"
[resetToDefaults]="intl.toggleResetToDefaults"
[toggleTitle]="intl.toggleTitle"
[togglePlaceholderTitle]="intl.togglePlaceholderTitle"
[useLegacyDesign]="useLegacyDesign"
[showDivider]="!useLegacyDesign && (displayToggleColumnsDivider$ | async) ?? false"
(visibleColumns)="visibilityManager.update($event)"
(resetColumns)="visibilityManager.reset()"
(visibleColumnsToggled)="visibleColumnsToggle$.next($event)">
</ui-grid-toggle-columns>
</ng-template>
./ui-grid.component.scss
@import "../../styles/ellipse";
.preserve-whitespace {
word-wrap: break-word;
white-space: pre-line;
text-align: left;
}
ui-grid {
$ui-grid-header-row-height: 56px;
$ui-grid-header-alternate-row-height: 52px;
$ui-grid-row-height: 48px;
$ui-grid-row-horizontal-padding: 24px;
$ui-feature-cell-width: 50px;
$ui-row-border-width: 1px;
$ui-header-resize-handle-width: 15px;
$ui-grid-default-spacing: 5px;
$ui-border-radius: 4px;
$ui-grid-button-size: 40px;
$ui-grid-main-font-size: 13.5px;
$ui-grid-secondary-font-size: 12px;
$ui-grid-action-buttons-spacing: 16px;
position: relative;
display: block;
&.ui-grid-mode-multi-select {
.ui-grid-table {
cdk-virtual-scroll-viewport.ui-grid-viewport {
min-height: $ui-grid-row-height * 3;
height: calc(100vh - 300px - #{$ui-grid-row-height});
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
}
}
.ui-grid-table {
cdk-virtual-scroll-viewport.ui-grid-viewport {
min-height: $ui-grid-row-height * 3;
height: calc(100vh - 300px);
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
}
.ui-grid-debug-information {
position: absolute;
text-align: right;
bottom: -30px;
width: 400px;
right: 0;
}
&.ui-grid-state-projected {
.ui-grid-action-button {
right: 80px;
}
.ui-grid {
&-table {
$ui-grid-projected-spacing-lr: 24px;
$ui-grid-projected-spacing-tb: 8px;
.ui-grid-cell-mobile-title {
width: 115px;
display: inline-block;
font-weight: bold;
text-align: left;
}
.ui-grid-header-cell {
&:not(.ui-grid-refresh-cell):not(.ui-grid-checkbox-cell):not(.ui-grid-selection-info-container) {
display: none;
}
&.ui-grid-refresh-cell {
justify-content: flex-end;
margin-right: $ui-grid-projected-spacing-lr;
}
&.ui-grid-checkbox-cell {
overflow: visible;
justify-content: flex-start;
margin-left: $ui-grid-projected-spacing-lr + $ui-grid-default-spacing;
}
}
.ui-grid-row {
height: auto;
flex-direction: column;
align-items: start;
padding: $ui-grid-projected-spacing-tb $ui-grid-projected-spacing-lr;
}
.ui-grid-cell.ui-grid-mobile-feature-cell {
margin-left: $ui-grid-default-spacing;
overflow: visible;
justify-content: flex-start;
.ui-grid-mobile-feature-container {
margin: 0 $ui-grid-default-spacing;
&:first-of-type {
margin-left: 0;
}
}
}
.ui-grid-cell {
justify-content: space-between;
padding: 0;
width: 100% !important;
}
}
}
.mat-paginator-container {
justify-content: center;
}
}
.mat-paginator-container {
padding: 0 $ui-grid-default-spacing;
}
&.ui-grid-state-resizing {
.ui-grid-header-row {
user-select: none;
}
}
.ui-grid {
&-primary,
&-header-cell {
font-weight: 500;
}
&-filter-container {
padding: 0 0 $ui-grid-default-spacing $ui-grid-default-spacing;
min-height: $ui-grid-header-row-height;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
&-lhs-group {
display: flex;
flex-direction: column;
&-actions {
min-height: $ui-grid-header-row-height;
display: flex;
flex-wrap: wrap;
align-items: center;
}
&-filters {
min-height: $ui-grid-header-row-height;
flex-wrap: wrap;
align-items: center;
display: flex;
}
}
&-rhs-group {
min-height: $ui-grid-header-row-height;
display: flex;
margin-left: auto;
align-items: center;
.ui-grid-action-buttons-main {
display: flex;
gap: $ui-grid-action-buttons-spacing;
}
}
}
// single main button on default design
&-action-button {
position: absolute;
right: $ui-grid-button-size;
top: -21px;
z-index: 2;
}
&-action-buttons {
&-main {
margin-left: auto;
}
}
&-container {
border-radius: $ui-border-radius;
.mat-paginator {
&-container {
min-height: $ui-grid-header-row-height - 1;
}
.mat-paginator-page-size-label,
.mat-paginator-range-label,
.mat-select-value {
font-size: $ui-grid-secondary-font-size;
line-height: $ui-grid-secondary-font-size;
}
.mat-form-field-infix {
padding: 0;
.mat-select {
padding: 0.4375em 0;
}
}
}
&.use-alternate-design {
.ui-grid-header {
&-row,
&-cell {
height: $ui-grid-header-alternate-row-height;
}
}
.ui-grid-selection-info {
&-clear-button {
order: 1;
}
}
.ui-grid-no-data-container {
height: 4 * $ui-grid-row-height;
align-items: flex-start;
&.ui-grid-no-content-available {
align-items: center;
justify-content: center;
flex-direction: column;
}
}
ui-grid-custom-paginator {
.mat-paginator-page-label {
font-size: $ui-grid-secondary-font-size;
}
.mat-paginator-page-size .mat-form-field {
padding: unset;
margin: unset;
.mat-select {
padding: 0.4375em 0;
}
}
}
}
}
&-table-container {
position: relative;
}
&-no-data-container {
padding: $ui-grid-default-spacing;
mat-icon {
font-size: 32px;
width: 32px;
height: 32px;
margin-left: $ui-grid-default-spacing;
}
p {
margin-left: $ui-grid-default-spacing;
}
}
&-selection-info-container {
&-alternate {
min-height: $ui-grid-button-size;
padding: $ui-grid-default-spacing 0;
display: flex;
align-items: center;
.ui-grid-selection {
&-clear-button {
order: 1;
}
}
}
button.mat-icon-button,
.ui-grid-selection-info-icon {
margin-left: 5px;
}
.ui-grid-selection-info-message {
margin-left: $ui-grid-default-spacing * 2;
}
.ui-grid-selection-info-icon {
// simulate same size as button
outline: none;
padding: 8px;
}
&.ui-grid-header-cell {
width: 100%;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
margin-right: $ui-grid-button-size + $ui-grid-row-horizontal-padding;
}
}
&-resize-anchor {
height: 100%;
width: $ui-header-resize-handle-width + $ui-grid-default-spacing;
cursor: col-resize;
justify-content: center;
display: flex;
align-items: center;
mat-icon {
margin-right: $ui-grid-default-spacing;
}
}
&-dropdown-filter {
&-button {
text-transform: none !important;
font-size: 0.8rem;
height: $ui-grid-button-size;
line-height: normal;
padding-right: 6px;
.ui-grid-dropdown-filter-title {
font-weight: 700;
}
}
}
&-filter-option {
margin-right: $ui-grid-default-spacing * 2;
&.ui-grid-search {
$search-padding: $ui-grid-default-spacing;
bottom: $search-padding;
position: relative;
height: $ui-grid-header-row-height - $search-padding - 1px;
}
&:last-child {
margin-right: 0;
}
}
&-table {
display: block;
.ui-grid-header-title {
width: calc(100% - #{$ui-header-resize-handle-width + $ui-grid-default-spacing});
height: 100%;
display: inline-flex;
align-items: center;
p {
@extend %ellipse;
}
}
.ui-grid-cell-content {
@extend %ellipse;
> * {
@extend %ellipse;
}
}
.ui-grid-header-row {
font-size: $ui-grid-secondary-font-size;
line-height: $ui-grid-secondary-font-size;
text-transform: uppercase;
height: $ui-grid-header-row-height;
position: relative;
}
.ui-grid-header {
mat-progress-bar {
position: absolute;
right: 0;
}
}
.ui-grid-expanded-row-container {
width: 100%;
height: 100%;
padding: $ui-grid-default-spacing;
box-sizing: border-box;
}
.ui-grid-row {
height: $ui-grid-row-height;
font-size: $ui-grid-main-font-size;
&.ui-grid-row-state-expanded {
height: auto;
}
}
.ui-grid-header-row,
.ui-grid-row,
.ui-grid-row-state-expanded {
display: flex;
align-items: center;
border-width: 0;
box-sizing: border-box;
&:not(.ui-grid-border-none) {
border-style: solid;
border-bottom-width: 1px;
}
}
&:not(.ui-grid-table-refreshable) {
.ui-grid-action-cell {
min-width: $ui-feature-cell-width;
}
}
.ui-grid-cell,
.ui-grid-header-cell {
flex: 1;
flex-basis: auto;
display: flex;
align-items: center;
overflow: hidden;
word-wrap: break-word;
&:not(.ui-grid-feature-cell):not(:first-child) {
box-sizing: border-box;
padding: 0 0 0 $ui-grid-default-spacing;
}
&:not(.ui-grid-feature-cell):first-of-type {
padding-left: $ui-grid-row-horizontal-padding;
[dir="rtl"] & {
padding-left: 0;
padding-right: $ui-grid-row-horizontal-padding;
}
}
&:not(.ui-grid-feature-cell):last-of-type {
padding-right: $ui-grid-row-horizontal-padding;
[dir="rtl"] & {
padding-right: 0;
padding-left: $ui-grid-row-horizontal-padding;
}
}
&.ui-grid-refresh-cell,
&.ui-grid-checkbox-cell {
min-width: $ui-feature-cell-width;
justify-content: center;
}
&.ui-grid-action-cell {
position: relative;
width: 0;
padding: 0;
overflow: visible;
> div {
display: inline-flex;
justify-content: flex-end;
align-items: center;
position: absolute;
height: $ui-grid-row-height - $ui-row-border-width;
min-width: $ui-feature-cell-width;
padding-right: $ui-grid-default-spacing;
right: 0;
bottom: 0;
}
}
}
.ui-grid-sort {
&-indicator {
height: $ui-grid-header-row-height - $ui-row-border-width;
align-items: center;
display: flex;
//sorted asc
&-asc {
// desc path
.path-desc {
opacity: 0;
}
}
&-desc {
//sorted desc
.path-asc {
// asc path
opacity: 0;
}
}
}
}
.ui-grid-header-cell {
height: $ui-grid-header-row-height - $ui-row-border-width;
}
.ui-grid-cell {
height: $ui-grid-row-height - $ui-row-border-width;
}
.ui-grid-header-cell-sortable {
cursor: pointer;
}
.ui-grid-cards-container {
margin: 16px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
grid-column-gap: 12px;
grid-row-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
}
.ui-grid-card-default {
border-radius: 5px;
background: #ffffff;
border: 1px solid #cfd8dd;
color: #273139;
padding: 16px;
}
.ui-grid-card-default-cell-content {
display: flex;
align-items: center;
gap: 8px;
}
}
}
}