Angular Grid 원격 데이터 작업
Ignite UI for Angular 원격 가상화, 원격 정렬, 원격 필터링 등과 같은 원격 데이터 작업을 지원합니다. 이를 통해 개발자는 서버에서 이러한 작업을 수행하고 생성된 데이터를 검색하여 Grid에 표시할 수 있습니다.
Angular Grid 원격 데이터 작업 개요 예제
import { ChangeDetectorRef, Component, OnInit, ViewChild, AfterViewInit, OnDestroy, inject } from '@angular/core' ;
import { IgxGridComponent, NoopFilteringStrategy, NoopSortingStrategy, IgxColumnComponent, IgxCellHeaderTemplateDirective, IgxCellTemplateDirective, IgxBadgeComponent } from 'igniteui-angular' ;
import { Subject } from 'rxjs' ;
import { debounceTime, takeUntil } from 'rxjs/operators' ;
import { RemoteFilteringService } from '../../services/remoteFiltering.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
import { AsyncPipe } from '@angular/common' ;
const DEBOUNCE_TIME = 300 ;
@Component ({
providers : [RemoteFilteringService],
selector : 'app-grid-remote-filtering-sample' ,
styleUrls : ['./remote-filtering-sample.component.scss' ],
templateUrl : './remote-filtering-sample.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellHeaderTemplateDirective, IgxCellTemplateDirective, IgxBadgeComponent, AsyncPipe]
})
export class RemoteFilteringSampleComponent implements OnInit , AfterViewInit , OnDestroy {
private _remoteService = inject(RemoteFilteringService);
cdr = inject(ChangeDetectorRef);
@ViewChild ('grid' , { static : true }) public grid: IgxGridComponent;
public remoteData: any ;
public noopFilterStrategy = NoopFilteringStrategy.instance();
public noopSortStrategy = NoopSortingStrategy.instance();
private _prevRequest: any ;
private _chunkSize: number ;
private destroy$ = new Subject<void >();
public ngOnInit(): void {
this .remoteData = this ._remoteService.remoteData;
}
public ngAfterViewInit ( ) {
const filteringExpr = this .grid.filteringExpressionsTree.filteringOperands;
const sortingExpr = this .grid.sortingExpressions[0 ];
this ._chunkSize = Math .ceil(parseInt (this .grid.height, 10 ) / this .grid.rowHeight);
this .grid.isLoading = true ;
this ._remoteService.getData(
{
chunkSize : this ._chunkSize,
startIndex : this .grid.virtualizationState.startIndex
},
filteringExpr,
sortingExpr,
(data ) => {
this .grid.totalItemCount = data['@odata.count' ];
this .grid.isLoading = false ;
});
this .grid.dataPreLoad.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData();
});
this .grid.filteringExpressionsTreeChange.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData(true );
});
this .grid.sortingExpressionsChange.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData();
});
}
public processData (isFiltering: boolean = false ) {
if (this ._prevRequest) {
this ._prevRequest.unsubscribe();
}
if (!this .grid.isLoading) {
this .grid.isLoading = true ;
}
const virtualizationState = this .grid.virtualizationState;
const filteringExpr = this .grid.filteringExpressionsTree.filteringOperands;
const sortingExpr = this .grid.sortingExpressions[0 ];
if (isFiltering) {
virtualizationState.startIndex = 0 ;
}
this ._prevRequest = this ._remoteService.getData(
{
chunkSize : this ._chunkSize,
startIndex : virtualizationState.startIndex
},
filteringExpr,
sortingExpr,
(data ) => {
this .grid.totalItemCount = data['@odata.count' ];
if (this .grid.isLoading) {
this .grid.isLoading = false ;
}
});
}
public formatNumber (value: number ) {
return value.toFixed(2 );
}
public formatCurrency (value: number ) {
return '$' + value.toFixed(2 );
}
public ngOnDestroy ( ) {
if (this ._prevRequest) {
this ._prevRequest.unsubscribe();
}
this .destroy$.next();
this .destroy$.complete();
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid [data ]="remoteData | async" [height ]="'460px'" [width ]="'100%'" [autoGenerate ]="false"
[filterStrategy ]="noopFilterStrategy"
[sortStrategy ]="noopSortStrategy"
[allowFiltering ]="true" >
<igx-column [field ]="'ProductID'" [sortable ]="true" [filterable ]="false" > </igx-column >
<igx-column [field ]="'ProductName'" [sortable ]="true" > </igx-column >
<igx-column [field ]="'UnitPrice'" dataType ="number" [formatter ]="formatCurrency" [sortable ]="true" > </igx-column >
<igx-column [field ]="'UnitsInStock'" dataType ="number" [headerClasses ]="'headerAlignSyle'" [sortable ]="true" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > UnitsInStock</span >
</ng-template >
<ng-template igxCell let-val >
<div class ="currency-badge-container" >
@if (val>50) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=50) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>50" [class.down ]="val<=50" > {{ formatNumber(val) }}</span >
</div >
</ng-template >
</igx-column >
<igx-column [field ]="'QuantityPerUnit'" [sortable ]="true" > </igx-column >
<igx-column [field ]="'ReorderLevel'" dataType ="number" [headerClasses ]="'headerAlignSyle'" [sortable ]="true" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > ReorderLevel</span >
</ng-template >
<ng-template igxCell let-val >
<div class ="currency-badge-container" >
@if (val>20) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=20) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>0" [class.down ]="val<=0" > {{ formatNumber(val) }}</span >
</div >
</ng-template >
</igx-column >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
.cellAlignSyle {
text-align : right;
float :right ;
}
.cellAlignSyle > span {
float :right ;
}
.up {
color : green;
}
.down {
color : red;
}
.headerAlignSyle {
text-align : right !important ;
}
.currency-badge-container {
width : 80px ;
float : right;
}
.badge-left {
float : left;
}
scss コピー
이 샘플이 마음에 드시나요? 전체 Ignite UI for Angular 툴킷에 액세스하고 몇 분 안에 나만의 앱을 구축해 보세요. 무료로 다운로드하세요.
기본적으로 Grid는 데이터 작업을 수행하기 위해 자체 논리를 사용합니다. 이러한 작업을 원격으로 수행하고 그리드에 의해 노출되는 특정 입력 및 이벤트를 활용하여 결과 데이터를 그리드에 공급할 수 있습니다.
원격 가상화
IgxGrid 는 원격 서비스에서 데이터 청크를 요청하는 시나리오를 지원하여 내부적으로 사용하는 igxForOf
지시문에 구현된 동작을 노출합니다.
이 기능을 활용하려면 dataPreLoad
출력을 구독하여 수신된 인수를 기반으로 적절한 요청을 수행하고 서비스에서 제공되는 해당 정보로 공용 IgxGrid 속성 totalItemCount
설정해야 합니다.
<igx-grid #grid [data ]="remoteData | async" [autoGenerate ]="false"
(dataPreLoad )="processData(false)"
(sortingDone )="processData(true)" >
<igx-column [field ]="'ProductID'" [sortable ]="true" > </igx-column >
<igx-column [field ]="'ProductName'" [sortable ]="true" > </igx-column >
<igx-column [field ]="'UnitPrice'" [dataType ]="'number'" [formatter ]="formatCurrency" [sortable ]="true" > </igx-column >
</igx-grid >
html
public ngAfterViewInit ( ) {
this .grid.isLoading = true ;
this ._remoteService.getData(this .grid.virtualizationState, this .grid.sortingExpressions[0 ], true , (data ) => {
this .grid.totalItemCount = data['@odata.count' ];
this .grid.isLoading = false ;
});
}
public processData (reset ) {
if (this .prevRequest) {
this .prevRequest.unsubscribe();
}
this ._prevRequest = this ._remoteService.getData(this .grid.virtualizationState,
this .grid.sortingExpressions[0 ], reset, () => {
...
this .cdr.detectChanges();
});
}
typescript
데이터를 요청할 때 startIndex
및 chunkSize
속성을 제공하는 IForOfState
인터페이스를 활용해야 합니다.
첫 번째 chunkSize
항상 0이며 특정 애플리케이션 시나리오에 따라 사용자가 결정해야 합니다.
원격 가상화 데모
import { ChangeDetectorRef, Component, TemplateRef, ViewChild, OnInit, AfterViewInit, OnDestroy, inject } from '@angular/core' ;
import { IgxColumnComponent, IgxGridComponent, IgxCellTemplateDirective, IgxCellHeaderTemplateDirective, IgxBadgeComponent } from 'igniteui-angular' ;
import { debounceTime } from 'rxjs/operators' ;
import { RemoteServiceVirt } from '../../services/remoteVirtualization.service' ;
import { AsyncPipe } from '@angular/common' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
@Component ({
providers : [RemoteServiceVirt],
selector : 'app-grid-remote-virtualization-sample' ,
styleUrls : ['grid-sample-4.component.scss' ],
templateUrl : 'grid-sample-4.component.html' ,
imports : [IgxCellTemplateDirective, IgxGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellHeaderTemplateDirective, IgxBadgeComponent, AsyncPipe]
})
export class GridRemoteVirtualizationSampleComponent implements OnInit , AfterViewInit , OnDestroy {
private _remoteService = inject(RemoteServiceVirt);
cdr = inject(ChangeDetectorRef);
@ViewChild ('grid' , { static : true }) public grid: IgxGridComponent;
@ViewChild ('remoteDataLoadingLarge' , { read : TemplateRef, static : true })
public remoteDataLoadingLargeTemplate: TemplateRef<any >;
@ViewChild ('remoteDataLoadingMedium' , { read : TemplateRef, static : true })
public remoteDataLoadingMediumTemplate: TemplateRef<any >;
@ViewChild ('remoteDataLoadingSmall' , { read : TemplateRef, static : true })
public remoteDataLoadingSmallTemplate: TemplateRef<any >;
public remoteData: any ;
private _columnCellCustomTemplates: Map <IgxColumnComponent, TemplateRef<any >>;
private _prevRequest: any ;
public ngOnInit(): void {
this .remoteData = this ._remoteService.data;
this ._columnCellCustomTemplates = new Map <IgxColumnComponent, TemplateRef<any >>();
}
public ngAfterViewInit ( ) {
this .grid.isLoading = true ;
this ._remoteService.getData(this .grid.virtualizationState, this .grid.sortingExpressions[0 ], true ,
(data ) => {
this .grid.totalItemCount = data['@odata.count' ];
this .grid.isLoading = false ;
}, {
startIndex : this .grid.virtualizationState.startIndex,
chunkSize : 20
});
this .grid.dataPreLoad.pipe().subscribe(() => {
this ._remoteService.getDataFromCache(this .grid.virtualizationState,
this .grid.sortingExpressions[0 ], false , () => {
this .cdr.detectChanges();
});
});
this .grid.dataPreLoad.pipe(debounceTime(500 )).subscribe(() => {
this .processData(false );
});
}
public handlePreLoad ( ) {
this .processData(false );
}
public processData (reset ) {
if (this ._prevRequest) {
this ._prevRequest.unsubscribe();
}
let state;
if (!reset) {
state = {
startIndex : this .grid.virtualizationState.startIndex,
chunkSize : 20
};
}
this ._prevRequest = this ._remoteService.getData(this .grid.virtualizationState,
this .grid.sortingExpressions[0 ], reset, () => {
this .cdr.detectChanges();
}, state);
}
public formatNumber (value: number ) {
return value.toFixed(2 );
}
public formatCurrency (value: number ) {
return '$' + value.toFixed(2 );
}
public ngOnDestroy ( ) {
if (this ._prevRequest) {
this ._prevRequest.unsubscribe();
}
}
public getDataLoadingTemplate(): TemplateRef<any > {
const val = Math .floor(Math .random() * 3 ) + 1 ;
switch (val) {
case 1 : return this .remoteDataLoadingLargeTemplate;
case 2 : return this .remoteDataLoadingMediumTemplate;
case 3 : return this .remoteDataLoadingSmallTemplate;
}
}
}
ts コピー <ng-template #cellTemplate igxCell let-val let-data ="cell.row.data" >
@if (!data.emptyRec) {
<div >
{{ val }}
</div >
} @else {
<ng-template [ngTemplateOutlet ]="getDataLoadingTemplate()" > </ng-template >
}
</ng-template >
<div class ="grid__wrapper" >
<ng-template #remoteDataLoadingLarge >
<div class ="remote-data-loading-template-large" > </div >
</ng-template >
<ng-template #remoteDataLoadingMedium >
<div class ="remote-data-loading-template-medium" > </div >
</ng-template >
<ng-template #remoteDataLoadingSmall >
<div class ="remote-data-loading-template-small" > </div >
</ng-template >
<igx-grid [igxPreventDocumentScroll ]="true" #grid [data ]="remoteData | async" [height ]="'500px'" [width ]="'100%'" [autoGenerate ]='false'
(sortingDone )="processData(true)" >
<igx-column [field ]="'ProductID'" [sortable ]="true" [cellTemplate ]="cellTemplate" > </igx-column >
<igx-column [field ]="'ProductName'" [sortable ]="true" [cellTemplate ]="cellTemplate" > </igx-column >
<igx-column [field ]="'UnitPrice'" dataType ="number" [formatter ]="formatCurrency" [sortable ]="true" [cellTemplate ]="cellTemplate" > </igx-column >
<igx-column [field ]="'UnitsInStock'" dataType ="number" [headerClasses ]="'headerAlignSyle'" [sortable ]="true" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > UnitsInStock</span >
</ng-template >
<ng-template igxCell let-val let-cell ="cell" >
@if (!cell.row.data.emptyRec) {
<div >
<div class ="currency-badge-container" >
@if (val>50) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=50) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>50" [class.down ]="val<=50" > {{ formatNumber(val) }}</span >
</div >
</div >
} @else {
<ng-template [ngTemplateOutlet ]="getDataLoadingTemplate()" > </ng-template >
}
</ng-template >
</igx-column >
<igx-column [field ]="'QuantityPerUnit'" [sortable ]="true" [cellTemplate ]="cellTemplate" > </igx-column >
<igx-column [field ]="'ReorderLevel'" dataType ="number" [headerClasses ]="'headerAlignSyle'" [sortable ]="true" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > ReorderLevel</span >
</ng-template >
<ng-template igxCell let-val let-cell ="cell" >
@if (!cell.row.data.emptyRec) {
<div >
<div class ="currency-badge-container" >
@if (val>20) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=20) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>0" [class.down ]="val<=0" > {{ formatNumber(val) }}</span >
</div >
</div >
} @else {
<ng-template [ngTemplateOutlet ]="getDataLoadingTemplate()" > </ng-template >
}
</ng-template >
</igx-column >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
.cellAlignSyle {
text-align : right;
float :right ;
}
.cellAlignSyle > span {
float :right ;
}
.up {
color : green;
}
.down {
color : red;
}
.headerAlignSyle {
text-align : right !important ;
}
.currency-badge-container {
width : 80px ;
float : right;
}
.badge-left {
float : left;
}
.remote-data-loading-template {
animation : content-placeholder-animation .5s infinite;
background-color : lightgray;
height : 15px ;
}
.remote-data-loading-template-medium {
@extend .remote-data-loading-template;
width : 30px ;
}
.remote-data-loading-template-large {
@extend .remote-data-loading-template;
width : 40px ;
}
.remote-data-loading-template-small {
@extend .remote-data-loading-template;
width : 20px ;
}
@keyframes content-placeholder-animation {
0% {
opacity : .75 ;
transform : scaleX(.9 );
}
50% {
opacity : 1 ;
transform : scaleX(1.1 );
}
100% {
opacity : .75 ;
transform : scaleX(.9 );
}
}
scss コピー
엔드포인트에서 청크로 데이터를 가져와야 하는 시나리오에 널리 사용되는 디자인은 소위 무한 스크롤입니다. 데이터 그리드의 경우 최종 사용자가 맨 아래까지 스크롤하면 로드된 데이터가 지속적으로 증가하는 것이 특징입니다. 다음 단락에서는 사용 가능한 API를 사용하여 IgxGrid
에서 무한 스크롤을 쉽게 달성하는 방법을 설명합니다.
무한 스크롤을 구현하려면 데이터를 청크로 가져와야 합니다. 이미 가져온 데이터는 로컬에 저장되어야 하며 청크의 길이와 청크 수를 결정해야 합니다. 또한 그리드에서 마지막으로 표시되는 데이터 행 인덱스를 추적해야 합니다. 이러한 방식으로 startIndex
및 chunkSize
속성을 사용하면 사용자가 위로 스크롤하여 이미 가져온 데이터를 표시해야 하는지 아니면 아래로 스크롤하여 끝점에서 더 많은 데이터를 가져와야 하는지 결정할 수 있습니다.
가장 먼저 해야 할 일은 ngAfterViewInit
수명 주기 후크를 사용하여 데이터의 첫 번째 청크를 가져오는 것입니다. totalItemCount
속성을 설정하는 것은 그리드의 스크롤 막대 크기를 올바르게 조정할 수 있도록 해주기 때문에 중요합니다.
public ngAfterViewInit ( ) {
this ._remoteService.loadDataForPage(this .page, this .pageSize, (request ) => {
if (request.data) {
this .grid.totalItemCount = this .page * this .pageSize;
this .grid.data = this ._remoteService.getCachedData({startIndex : 0 , chunkSize : 10 });
this .totalItems = request.data['@odata.count' ];
this .totalPageCount = Math .ceil(this .totalItems / this .pageSize);
this .grid.isLoading = false ;
}
});
}
typescript
또한 현재 로드된 청크가 아닌 다른 청크를 표시하려고 할 때 그리드에 필요한 데이터를 제공할 수 있도록 dataPreLoad
출력을 구독해야 합니다. 이벤트 핸들러에서는 새 데이터를 가져올지 아니면 이미 로컬에 캐시된 데이터를 반환할지 결정해야 합니다.
public handlePreLoad ( ) {
const isLastChunk = this .grid.totalItemCount ===
this .grid.virtualizationState.startIndex + this .grid.virtualizationState.chunkSize;
if (isLastChunk) {
if (this .totalPageCount === this .page) {
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
return ;
}
this .page++;
this .grid.isLoading = true ;
this ._remoteService.loadDataForPage(this .page, this .pageSize, (request ) => {
if (request.data) {
this .grid.totalItemCount = Math .min(this .page * this .pageSize, this .totalItems);
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
this .grid.isLoading = false ;
}
});
} else {
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
}
}
typescript
import { AfterViewInit, ChangeDetectorRef, Component, ViewChild, OnInit, inject } from '@angular/core' ;
import { IgxGridComponent, IgxColumnComponent, IgxCellHeaderTemplateDirective, IgxCellTemplateDirective, IgxBadgeComponent } from 'igniteui-angular' ;
import { RemoteService } from '../../services/remote.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
@Component ({
providers : [RemoteService],
selector : 'app-grid-remote-virtualization-sample' ,
styleUrls : ['grid-sample-5.component.scss' ],
templateUrl : 'grid-sample-5.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellHeaderTemplateDirective, IgxCellTemplateDirective, IgxBadgeComponent]
})
export class GridRemoteVirtualizationAddRowSampleComponent implements AfterViewInit , OnInit {
private _remoteService = inject(RemoteService);
cdr = inject(ChangeDetectorRef);
@ViewChild ('grid' , { static : true })
public grid: IgxGridComponent;
public remoteData: any ;
private page = 1 ;
private pageSize = 10 ;
private totalPageCount = 0 ;
private totalItems = 0 ;
public ngOnInit(): void {
this .remoteData = this ._remoteService.data;
}
public ngAfterViewInit ( ) {
this .grid.isLoading = true ;
const dataViewSize = parseInt (this .grid.height, 10 ) / this .grid.rowHeight;
this .pageSize = Math .floor(dataViewSize * 1.5 );
this ._remoteService.loadDataForPage(this .page, this .pageSize, (request ) => {
if (request.data) {
this .grid.data = this ._remoteService.getCachedData({startIndex : 0 , chunkSize : 10 });
this .cdr.detectChanges();
this .grid.verticalScrollContainer.totalItemCount = this .page * this .pageSize;
this .totalItems = request.data['@odata.count' ];
this .totalPageCount = Math .ceil(this .totalItems / this .pageSize);
this .grid.isLoading = false ;
}
});
}
public handlePreLoad ( ) {
const isLastChunk = this .grid.totalItemCount ===
this .grid.virtualizationState.startIndex + this .grid.virtualizationState.chunkSize;
if (isLastChunk) {
if (this .totalPageCount === this .page) {
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
return ;
}
this .page++;
this .grid.isLoading = true ;
this ._remoteService.loadDataForPage(this .page, this .pageSize, (request ) => {
if (request.data) {
this .grid.totalItemCount = Math .min(this .page * this .pageSize, this .totalItems);
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
this .grid.isLoading = false ;
}
});
} else {
this .grid.data = this ._remoteService.getCachedData(this .grid.virtualizationState);
}
}
public formatNumber (value: number ) {
return value.toFixed(2 );
}
public formatCurrency (value: number ) {
return '$' + value.toFixed(2 );
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid [height ]="'480px'" [width ]="'100%'" [autoGenerate ]='false' (dataPreLoad )="handlePreLoad()" >
<igx-column [field ]="'ProductID'" > </igx-column >
<igx-column [field ]="'ProductName'" > </igx-column >
<igx-column [field ]="'UnitPrice'" dataType ="number" [formatter ]="formatCurrency" > </igx-column >
<igx-column [field ]="'UnitsInStock'" dataType ="number" [headerClasses ]="'headerAlignSyle'" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > UnitsInStock</span >
</ng-template >
<ng-template igxCell let-val >
<div class ="currency-badge-container" >
@if (val>50) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=50) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>50" [class.down ]="val<=50" > {{ formatNumber(val) }}</span >
</div >
</ng-template >
</igx-column >
<igx-column [field ]="'QuantityPerUnit'" > </igx-column >
<igx-column [field ]="'ReorderLevel'" dataType ="number" [headerClasses ]="'headerAlignSyle'" >
<ng-template igxHeader >
<span class ="cellAlignSyle" > ReorderLevel</span >
</ng-template >
<ng-template igxCell let-val >
<div class ="currency-badge-container" >
@if (val>20) {
<igx-badge type ="success" position ="bottom-right" icon ="arrow_upward" class ="badge-left" > </igx-badge >
}
@if (val<=20) {
<igx-badge type ="error" position ="bottom-right" icon ="arrow_downward" class ="error badge-left" > </igx-badge >
}
<span class ="cellAlignSyle" [class.up ]="val>0" [class.down ]="val<=0" > {{ formatNumber(val) }}</span >
</div >
</ng-template >
</igx-column >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
.cellAlignSyle {
text-align : right;
float :right ;
}
.cellAlignSyle > span {
float :right ;
}
.up {
color : green;
}
.down {
color : red;
}
.headerAlignSyle {
text-align : right !important ;
}
.currency-badge-container {
width : 80px ;
float : right;
}
.badge-left {
float : left;
}
.remote-data-loading-template {
animation : content-placeholder-animation .5s infinite;
background-color : lightgray;
height : 15px ;
}
.remote-data-loading-template-medium {
@extend .remote-data-loading-template;
width : 30px ;
}
.remote-data-loading-template-large {
@extend .remote-data-loading-template;
width : 40px ;
}
.remote-data-loading-template-small {
@extend .remote-data-loading-template;
width : 20px ;
}
@keyframes content-placeholder-animation {
0% {
opacity : .75 ;
transform : scaleX(.9 );
}
50% {
opacity : 1 ;
transform : scaleX(1.1 );
}
100% {
opacity : .75 ;
transform : scaleX(.9 );
}
}
scss コピー
원격 정렬/필터링
원격 정렬 및 필터링을 제공하려면 dataPreLoad
, sortingExpressionsChange
및 filteringExpressionsTreeChange
출력을 구독하여 수신된 인수를 기반으로 적절한 요청을 만들고 서비스에서 제공되는 해당 정보로 공용 IgxGrid 속성 totalItemCount
설정해야 합니다. .
또한 다른 소스 방출 없이 특정 시간 범위가 경과한 후에만 소스 Observable에서 값을 방출하는 rxjs debounceTime
함수를 활용할 것입니다. 이렇게 하면 사용자가 방해하지 않고 지정된 시간이 경과한 경우에만 원격 작업이 트리거됩니다.
const DEBOUNCE_TIME = 300 ;
...
public ngAfterViewInit ( ) {
...
this .grid.dataPreLoad.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData();
});
this .grid.filteringExpressionsTreeChange.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData(true );
});
this .grid.sortingExpressionsChange.pipe(
debounceTime(DEBOUNCE_TIME),
takeUntil(this .destroy$)
).subscribe(() => {
this .processData();
});
}
typescript
원격 정렬 및 필터링이 제공되면 일반적으로 그리드의 기본 정렬 및 필터링이 필요하지 않습니다. 그리드의 sortStrategy
및 filterStrategy
입력을 NoopSortingStrategy
및 NoopFilteringStrategy
각각의 인스턴스로 설정하여 이를 비활성화할 수 있습니다.
<igx-grid #grid [data ]="remoteData | async" [height ]="'500px'" [width ]="'100%'" [autoGenerate ]='false'
[filterStrategy ]="noopFilterStrategy"
[sortStrategy ]="noopSortStrategy"
[allowFiltering ]="true" >
...
</igx-grid >
html
public noopFilterStrategy = NoopFilteringStrategy.instance();
public noopSortStrategy = NoopSortingStrategy.instance();
typescript
원격 데이터가 요청되면 필터링 작업은 대소문자를 구분합니다.
원격 정렬/필터링 데모
데모 섹션의 이 문서 시작 부분에서 위의 코드 결과를 볼 수 있습니다.
고유한 열 값 전략
Excel 스타일 필터링 대화 상자 내의 목록 항목은 해당 열의 고유 값을 나타냅니다. 그리드는 기본적으로 데이터 소스를 기반으로 이러한 값을 생성합니다. 원격 필터링의 경우 그리드 데이터에는 서버의 모든 데이터가 포함되지 않습니다. 고유한 값을 수동으로 제공하고 요청 시 로드하기 위해 Grid의 uniqueColumnValuesStrategy
입력을 활용할 수 있습니다. 이 입력은 실제로 세 가지 인수를 제공하는 메서드입니다.
열 - 해당 열 인스턴스입니다.
filteringExpressionsTree - 해당 열을 기준으로 축소된 필터링 표현식 트리입니다.
done - 서버에서 검색될 때 새로 생성된 열 값으로 호출되어야 하는 콜백입니다.
개발자는 열과 filteringExpressionsTree 인수에서 제공되는 정보를 기반으로 필요한 고유 열 값을 수동으로 생성한 다음 완료 콜백을 호출할 수 있습니다.
<igx-grid #grid1 [data ]="data" [filterMode ]="'excelStyleFilter'" [uniqueColumnValuesStrategy ]="columnValuesStrategy" >
...
</igx-grid >
html
public columnValuesStrategy = (column: ColumnType,
columnExprTree: IFilteringExpressionsTree,
done: (uniqueValues: any []) => void ) => {
this .remoteValuesService.getColumnData(column, columnExprTree, uniqueValues => done(uniqueValues));
}
typescript
고유한 열 값 전략 데모
import { Component, OnInit, inject } from '@angular/core' ;
import { IFilteringExpressionsTree, IgxColumnComponent, IgxGridComponent, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent } from 'igniteui-angular' ;
import { RemoteValuesService } from './remoteValues.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
@Component ({
selector : 'app-grid-excel-style-filtering-load-on-demand' ,
styleUrls : ['./grid-excel-style-filtering-load-on-demand.component.scss' ],
templateUrl : './grid-excel-style-filtering-load-on-demand.component.html' ,
providers : [RemoteValuesService],
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxColumnComponent]
})
export class GridExcelStyleFilteringLoadOnDemandComponent implements OnInit {
private remoteValuesService = inject(RemoteValuesService);
public data: any [];
public columnValuesStrategy = (column: IgxColumnComponent,
columnExprTree: IFilteringExpressionsTree,
done: (uniqueValues: any []) => void ) => {
this .remoteValuesService.getColumnData(column, columnExprTree, uniqueValues => done(uniqueValues));
};
public ngOnInit ( ) {
this .data = this .remoteValuesService.getRecordsData();
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid1 [data ]="data" [moving ]="true" [height ]="'750px'"
[allowFiltering ]="true" [filterMode ]="'excelStyleFilter'"
[uniqueColumnValuesStrategy ]="columnValuesStrategy" >
<igx-grid-toolbar >
<igx-grid-toolbar-actions >
<igx-grid-toolbar-hiding > </igx-grid-toolbar-hiding >
<igx-grid-toolbar-pinning > </igx-grid-toolbar-pinning >
</igx-grid-toolbar-actions >
</igx-grid-toolbar >
<igx-column [field ]="'ID'" [filterable ]="true" [sortable ]="true" [dataType ]="'string'" > </igx-column >
<igx-column [field ]="'CompanyName'" [filterable ]="true" [sortable ]="true" [dataType ]="'string'" > </igx-column >
<igx-column [field ]="'Employees'" [filterable ]="true" [sortable ]="true" [dataType ]="'number'" > </igx-column >
<igx-column [field ]="'Contract'" [filterable ]="true" [sortable ]="true" [dataType ]="'boolean'" > </igx-column >
<igx-column [field ]="'DateCreated'" [filterable ]="true" [sortable ]="true" [dataType ]="'date'" > </igx-column >
</igx-grid >
</div >
html コピー .grid__wrapper {
--ig-size: var(--ig-size-medium);
margin : 0 auto;
padding : 16px ;
}
scss コピー
Excel 스타일 필터링을 위한 사용자 정의 로딩 템플릿을 제공하기 위해 igxExcelStyleLoading
지시문을 사용할 수 있습니다.
<igx-grid [data ]="data" [filterMode ]="'excelStyleFilter'" [uniqueColumnValuesStrategy ]="columnValuesStrategy" >
...
<ng-template igxExcelStyleLoading >
Loading ...
</ng-template >
</igx-grid >
html
원격 페이징
페이징 기능은 원격 데이터로 작동할 수 있습니다. 이를 시연하기 위해 먼저 데이터 가져오기를 담당할 서비스를 선언하겠습니다. 페이지 수를 계산하려면 모든 데이터 항목의 수가 필요합니다. 이 로직은 우리 서비스에 추가될 예정입니다.
@Injectable ()
export class RemotePagingService {
public remoteData: BehaviorSubject<any []>;
public dataLenght: BehaviorSubject<number > = new BehaviorSubject(0 );
public url = 'https://www.igniteui.com/api/products' ;
constructor (private http: HttpClient ) {
this .remoteData = new BehaviorSubject([]) as any ;
}
public getData(index?: number , perPage?: number ): any {
let qS = '' ;
if (perPage) {
qS = `?$skip=${index} &$top=${perPage} &$count=true` ;
}
this .http
.get(`${this .url + qS} ` ).pipe(
map((data: any ) => data)
).subscribe((data ) => this .remoteData.next(data));
}
public getDataLength(): any {
return this .http.get(this .url).pipe(
map((data: any ) => data.length)
);
}
}
typescript
서비스를 선언한 후에는 그리드 구성 및 데이터 구독을 담당할 구성 요소를 생성해야 합니다.
export class RemotePagingGridSample implements OnInit , AfterViewInit , OnDestroy {
public data: Observable<any []>;
private _dataLengthSubscriber;
constructor (private remoteService: RemoteService ) {}
public ngOnInit ( ) {
this .data = this .remoteService.remoteData.asObservable();
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data ) => {
this .totalCount = data;
this .grid1.isLoading = false ;
});
}
public ngOnDestroy ( ) {
if (this ._dataLengthSubscriber) {
this ._dataLengthSubscriber.unsubscribe();
}
}
}
typescript
이제 자체 사용자 정의 페이징 템플릿을 설정하거나 igx-paginator
제공하는 기본 템플릿을 사용하는 것 중에서 선택할 수 있습니다. 먼저 기본 페이징 템플릿을 사용하여 원격 페이징을 설정하는 데 필요한 것이 무엇인지 살펴보겠습니다.
기본 템플릿을 사용한 원격 페이징
기본 페이징 템플릿을 사용하려면 Paginator의 totalRecords
속성을 설정해야 합니다. 그래야만 그리드가 총 원격 레코드를 기반으로 총 페이지 수를 계산할 수 있습니다. 원격 페이지 매기기를 수행할 때 페이지네이터는 현재 페이지의 데이터만 그리드에 전달하므로 그리드는 제공된 데이터 소스에 페이지 매기기를 시도하지 않습니다. 그렇기 때문에 Grid의 pagingMode
속성을 GridPagingMode.remote 로 설정해야 합니다. 또한 원격 서비스에서 데이터를 가져오려면 pagingDone
또는 perPageChange
이벤트를 구독해야 하며, 어떤 이벤트가 사용될 것인지는 사용 사례에 따라 다릅니다.
<igx-grid #grid1 [data ]="data | async" [isLoading ]="isLoading" [pagingMode ]="mode" >
<igx-column field ="ID" > </igx-column >
...
<igx-paginator [(page )]="page" [(perPage )]="perPage" [totalRecords ]="totalCount"
(pagingDone )="paginate($event.current)" >
</igx-paginator >
</igx-grid >
html
public totalCount = 0 ;
public data: Observable<any []>;
public mode = GridPagingMode.remote;
public isLoading = true ;
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
private _dataLengthSubscriber;
public set perPage (val: number ) {
this ._perPage = val;
this .paginate(0 );
}
public ngOnInit ( ) {
this .data = this .remoteService.remoteData.asObservable();
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data: any ) => {
this .totalCount = data;
this .grid1.isLoading = false ;
});
}
public ngAfterViewInit ( ) {
const skip = this .page * this .perPage;
this .remoteService.getData(skip, this .perPage);
}
public paginate (page: number ) {
this .page = page;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
}
typescript
import { AfterViewInit, Component, OnDestroy, OnInit, TemplateRef, ViewChild, ViewEncapsulation, inject } from '@angular/core' ;
import { GridPagingMode, IgxGridComponent, IgxPaginatorComponent, IgxColumnComponent } from 'igniteui-angular' ;
import { Observable } from 'rxjs' ;
import { RemotePagingService } from '../../services/remotePaging.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
import { AsyncPipe } from '@angular/common' ;
@Component ({
encapsulation : ViewEncapsulation.None,
providers : [RemotePagingService],
selector : 'app-remote-paging-default-template' ,
styleUrls : ['./remote-paging-default-template.component.scss' ],
templateUrl : './remote-paging-default-template.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxPaginatorComponent, IgxColumnComponent, AsyncPipe]
})
export class RemotePagingDefaultTemplateComponent implements OnInit , AfterViewInit , OnDestroy {
private remoteService = inject(RemotePagingService);
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
@ViewChild ('customPager' , { read : TemplateRef, static : true }) public remotePager: TemplateRef<any >;
public totalCount = 0 ;
public page = 0 ;
public data: Observable<any []>;
public mode = GridPagingMode.Remote;
public isLoading = true ;
private _dataLengthSubscriber;
private _perPage = 10 ;
public get perPage (): number {
return this ._perPage;
}
public set perPage (val: number ) {
this ._perPage = val;
this .paginate(0 );
}
public ngOnInit ( ) {
this .data = this .remoteService.remoteData.asObservable();
this .data.subscribe(() => {
this .isLoading = false ;
});
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data ) => {
this .totalCount = data;
});
}
public ngOnDestroy ( ) {
if (this ._dataLengthSubscriber) {
this ._dataLengthSubscriber.unsubscribe();
}
}
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
this .remoteService.getData(0 , this .grid1.perPage);
this .remoteService.getDataLength();
}
public pagingDone (page ) {
const skip = page.current * this .grid1.perPage;
this .remoteService.getData(skip, this .grid1.perPage);
}
public paginate (page ) {
this .isLoading = true ;
const skip = page * this .grid1.perPage;
this .remoteService.getData(skip, this .grid1.perPage);
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid1 [data ]="data | async" width ="100%"
height ="580px" [isLoading ]="isLoading" [pagingMode ]="mode" >
<igx-paginator
[(page )]="page"
[(perPage )]="perPage"
[totalRecords ]="totalCount"
(pagingDone )="paginate($event.current)" >
</igx-paginator >
<igx-column field ="ID" > </igx-column >
<igx-column field ="ProductName" > </igx-column >
<igx-column field ="QuantityPerUnit" > </igx-column >
<igx-column field ="SupplierName" > </igx-column >
<igx-column field ="UnitsInStock" > </igx-column >
<igx-column field ="Rating" > </igx-column >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
scss コピー
사용자 정의 igx-paginator-content를 사용한 원격 페이징
사용자 정의 페이지네이터 콘텐츠를 정의할 때 요청된 페이지에 대한 데이터만 가져오고 올바른 페이지를 전달하는 방식으로 콘텐츠를 정의해야 합니다. 건너뛰다 그리고 맨 위 선택한 페이지 및 항목에 따라 원격 서비스에 대한 매개변수 perPage
. 우리는 <igx-paginator>
예제 구성을 쉽게 하기 위해 IgxPageSizeSelectorComponent
그리고 IgxPageNavigationComponent
소개되었던 -igx-page-size
페이지당 드롭다운과 라벨을 추가하고 igx-page-nav
탐색 작업 버튼과 라벨이 추가됩니다.
<igx-paginator #paginator
[totalRecords ]="totalCount"
[(page )]="page"
[(perPage )]="perPage"
[selectOptions ]="selectOptions"
(pageChange )="paginate($event)"
(perPageChange )="perPageChange($event)" >
<igx-paginator-content >
<igx-page-size > </igx-page-size >
[This is my custom content]
<igx-page-nav > </igx-page-nav >
</igx-paginator-content >
</igx-paginator >
html
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
private _perPage = 15 ;
private _dataLengthSubscriber: { unsubscribe : () => void ; } | undefined ;
constructor (private remoteService: RemotePagingService ) { }
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
this .remoteService.getData(0 , this .perPage);
}
public paginate (page: number ) {
this .page = page;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
}
public perPageChange (perPage: number ) {
const skip = this .page * perPage;
const top = perPage;
this .remoteService.getData(skip, top);
}
typescript
원격 페이징을 올바르게 구성하려면 GridPagingMode.Remote
설정해야 합니다.
<igx-grid #grid1 [data ]="data | async" width ="100%" height ="580px" [pagingMode ]="mode" > </igx-grid >
...
public mode = GridPagingMode.Remote;
html
마지막 단계는 요구 사항에 따라 페이지네이터 콘텐츠를 선언하는 것입니다.
<igx-paginator-content >
<igx-page-size > </igx-page-size >
[This is my custom content]
<igx-page-nav > </igx-page-nav >
</igx-paginator-content >
html
위의 모든 변경 후에는 다음과 같은 결과가 달성됩니다.
import { AfterViewInit, Component, OnDestroy, OnInit, TemplateRef, ViewChild, ViewEncapsulation, inject } from '@angular/core' ;
import { GridPagingMode, IgxGridComponent, IgxColumnComponent, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxPageSizeSelectorComponent, IgxPageNavigationComponent } from 'igniteui-angular' ;
import { Observable } from 'rxjs' ;
import { RemotePagingService } from '../../services/remotePaging.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
import { AsyncPipe } from '@angular/common' ;
@Component ({
encapsulation : ViewEncapsulation.None,
providers : [RemotePagingService],
selector : 'app-remote-paging-grid-sample' ,
styleUrls : ['./remote-paging-sample.component.scss' ],
templateUrl : './remote-paging-sample.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxPageSizeSelectorComponent, IgxPageNavigationComponent, AsyncPipe]
})
export class RemotePagingGridSampleComponent implements OnInit , AfterViewInit , OnDestroy {
private remoteService = inject(RemotePagingService);
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
public page = 0 ;
public totalCount = 0 ;
public pages = [];
public data: Observable<any []>;
public selectOptions = [5 , 10 , 15 , 25 , 50 ];
public mode = GridPagingMode.Remote;
private _perPage = 15 ;
private _dataLengthSubscriber;
public ngOnInit ( ) {
this .data = this .remoteService.remoteData.asObservable();
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data: any ) => {
this .totalCount = data;
this .grid1.isLoading = false ;
});
}
public get perPage (): number {
return this ._perPage;
}
public set perPage (val: number ) {
this ._perPage = val;
this .paginate(0 );
}
public ngOnDestroy ( ) {
if (this ._dataLengthSubscriber) {
this ._dataLengthSubscriber.unsubscribe();
}
}
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
this .remoteService.getData(0 , this .perPage);
}
public paginate (page: number ) {
this .page = page;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
}
public perPageChange (perPage: number ) {
const skip = this .page * perPage;
const top = perPage;
this .remoteService.getData(skip, top);
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid1 [data ]="data | async" width ="100%" height ="580px" [pagingMode ]="mode" >
<igx-column field ="ID" > </igx-column >
<igx-column field ="ProductName" > </igx-column >
<igx-column field ="QuantityPerUnit" > </igx-column >
<igx-column field ="SupplierName" > </igx-column >
<igx-column field ="UnitsInStock" > </igx-column >
<igx-column field ="Rating" > </igx-column >
<igx-paginator #paginator
[totalRecords ]="totalCount"
[(page )]="page"
[(perPage )]="perPage"
[selectOptions ]="selectOptions"
(pageChange )="paginate($event)"
(perPageChange )="perPageChange($event)" >
<igx-paginator-content >
<igx-page-size > </igx-page-size >
[This is my custom content]
<igx-page-nav > </igx-page-nav >
</igx-paginator-content >
</igx-paginator >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
scss コピー
사용자 정의 페이지네이터를 사용한 원격 페이징
경우에 따라 고유한 페이징 동작을 정의하고 싶을 수 있으며 이때 페이징 템플릿을 활용하고 사용자 지정 논리를 함께 추가할 수 있습니다. 이를 보여주기 위해 원격 페이징 예제를 확장하겠습니다.
import { AfterViewInit, Component, OnDestroy, OnInit, TemplateRef, ViewChild, ViewEncapsulation, inject } from '@angular/core' ;
import { IgxGridComponent, IgxColumnComponent, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxButtonDirective } from 'igniteui-angular' ;
import { Observable } from 'rxjs' ;
import { RemotePagingService } from '../../services/remotePaging.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
import { AsyncPipe } from '@angular/common' ;
import { RouterLink } from '@angular/router' ;
@Component ({
encapsulation : ViewEncapsulation.None,
providers : [RemotePagingService],
selector : 'app-custom-remote-paging-grid-sample' ,
styleUrls : ['./custom-remote-paging-sample.component.scss' ],
templateUrl : './custom-remote-paging-sample.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxButtonDirective, RouterLink, AsyncPipe]
})
export class CustomRemotePagingGridSampleComponent implements OnInit , AfterViewInit , OnDestroy {
private remoteService = inject(RemotePagingService);
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
public page = 0 ;
public lastPage = false ;
public firstPage = true ;
public totalPages = 1 ;
public totalCount = 0 ;
public pages = [];
public data: Observable<any []>;
private visibleElements = 5 ;
private _perPage = 10 ;
private _dataLengthSubscriber;
public get perPage (): number {
return this ._perPage;
}
public set perPage (val: number ) {
this ._perPage = val;
this .paginate(0 , true );
}
public get shouldShowLastPage () {
return this .pages[this .pages.length - 1 ] !== this .totalPages - 1 ;
}
public get shouldShowFirstPage () {
return this .pages[0 ] !== 0 ;
}
public ngOnInit ( ) {
this .data = this .remoteService.remoteData.asObservable();
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data ) => {
this .totalCount = data;
this .totalPages = Math .ceil(data / this .perPage);
this .buttonDeselection(this .page, this .totalPages);
this .grid1.isLoading = false ;
this .setNumberOfPagingItems(this .page, this .totalPages);
});
}
public ngOnDestroy ( ) {
if (this ._dataLengthSubscriber) {
this ._dataLengthSubscriber.unsubscribe();
}
}
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
this .remoteService.getData(0 , this .perPage);
}
public nextPage ( ) {
this .firstPage = false ;
this .page++;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
if (this .page + 1 >= this .totalPages) {
this .lastPage = true ;
}
this .setNumberOfPagingItems(this .page, this .totalPages);
}
public previousPage ( ) {
this .lastPage = false ;
this .page--;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
if (this .page <= 0 ) {
this .firstPage = true ;
}
this .setNumberOfPagingItems(this .page, this .totalPages);
}
public paginate (page: number , recalculate = false ) {
this .page = page;
const skip = this .page * this .perPage;
const top = this .perPage;
if (recalculate) {
this .totalPages = Math .ceil(this .totalCount / this .perPage);
}
this .setNumberOfPagingItems(this .page, this .totalPages);
this .remoteService.getData(skip, top);
this .buttonDeselection(this .page, this .totalPages);
}
public buttonDeselection (page: number , totalPages: number ) {
if (totalPages === 1 ) {
this .lastPage = true ;
this .firstPage = true ;
} else if (page + 1 >= totalPages) {
this .lastPage = true ;
this .firstPage = false ;
} else if (page !== 0 && page !== totalPages) {
this .lastPage = false ;
this .firstPage = false ;
} else {
this .lastPage = false ;
this .firstPage = true ;
}
}
public activePage (page ) {
return page === this .page ? 'activePage' : '' ;
}
public setNumberOfPagingItems (currentPage, totalPages ) {
if (currentPage > this .pages[0 ] && currentPage < this .pages[this .pages.length]) {
return ;
}
if (this .pages.length === 0 ) {
const lastPage = (currentPage + this .visibleElements) <= totalPages ?
currentPage + this .visibleElements : totalPages;
for (let item = 0 ; item < lastPage; item++) {
this .pages.push(item);
}
return ;
}
if (currentPage <= this .pages[0 ]) {
this .pages = [];
let firstPage = currentPage - 1 < 0 ? 0 : currentPage - 1 ;
firstPage = firstPage > totalPages - this .visibleElements ?
totalPages - this .visibleElements : firstPage;
firstPage = firstPage >= 0 ? firstPage : 0 ;
const lastPage = (firstPage + this .visibleElements) <= totalPages ?
firstPage + this .visibleElements : totalPages;
for (let item = firstPage; item < lastPage; item++) {
this .pages.push(item);
}
} else if (currentPage >= this .pages[this .pages.length - 1 ]) {
this .pages = [];
let firstPage = currentPage > totalPages - this .visibleElements ?
totalPages - this .visibleElements : currentPage - 1 ;
firstPage = firstPage >= 0 ? firstPage : 0 ;
const lastPage = (firstPage + this .visibleElements) <= totalPages ?
firstPage + this .visibleElements : totalPages;
for (let item = firstPage; item < lastPage; item++) {
this .pages.push(item);
}
}
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid1 [data ]="data | async" width ="100%" height ="580px" >
<igx-column field ="ID" > </igx-column >
<igx-column field ="ProductName" > </igx-column >
<igx-column field ="QuantityPerUnit" > </igx-column >
<igx-column field ="SupplierName" > </igx-column >
<igx-column field ="UnitsInStock" > </igx-column >
<igx-column field ="Rating" > </igx-column >
<igx-paginator [perPage ]="perPage" >
<igx-paginator-content >
<div class ="fullWidth" >
<div id ="numberPager" class ="fullWidth" >
<button [disabled ]="firstPage" (click )="previousPage()" igxButton ="flat" >
PREV
</button >
@if (shouldShowFirstPage) {
<span (click )="paginate(0)" >
<a class ="pageNavLinks" [routerLink ]="[]" > {{1}}</a > <span class ="pageNavLinks" > ...</span >
</span >
}
@for (item of pages; track item) {
<span (click )="paginate(item)" >
<a class ="pageNavLinks {{activePage(item)}}" [routerLink ]="[]" > {{item + 1}}</a >
</span >
}
@if (shouldShowLastPage) {
<span (click )="paginate(totalPages - 1)" >
<span class ="pageNavLinks" > ...</span > <a class ="pageNavLinks" [routerLink ]="[]" > {{ totalPages
}}</a >
</span >
}
<button [disabled ]="lastPage" (click )="nextPage()" igxButton ="flat" >
NEXT
</button >
</div >
</div >
</igx-paginator-content >
</igx-paginator >
</igx-grid >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
.pageNavLinks {
text-decoration : none;
font-size : 10.5pt ;
color : #989898 ;
}
.activePage {
font-size : 11.5pt ;
font-weight : bold;
}
.igx-input-group__input {
display : inline;
}
#numberPager {
display : flex;
align-items : center;
justify-content : center;
}
#numberPager span {
margin-left : .5rem ;
}
.fullWidth {
width : 100% ;
}
scss コピー
아래에서는 자체적인 next
및 previous
페이지 작업을 구현하기 위해 정의한 메서드를 찾을 수 있습니다.
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
this .remoteService.getData(0 , this .perPage);
}
public nextPage ( ) {
this .firstPage = false ;
this .page++;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
if (this .page + 1 >= this .totalPages) {
this .lastPage = true ;
}
this .setNumberOfPagingItems(this .page, this .totalPages);
}
public previousPage ( ) {
this .lastPage = false ;
this .page--;
const skip = this .page * this .perPage;
const top = this .perPage;
this .remoteService.getData(skip, top);
if (this .page <= 0 ) {
this .firstPage = true ;
}
this .setNumberOfPagingItems(this .page, this .totalPages);
}
public paginate (page: number , recalculate = false ) {
this .page = page;
const skip = this .page * this .perPage;
const top = this .perPage;
if (recalculate) {
this .totalPages = Math .ceil(this .totalCount / this .perPage);
}
this .setNumberOfPagingItems(this .page, this .totalPages);
this .remoteService.getData(skip, top);
this .buttonDeselection(this .page, this .totalPages);
}
typescript
일괄 편집을 통한 원격 페이징
지금까지의 예제를 통해 원격 데이터로 IgxGrid를 설정하는 방법을 명확히 했습니다. 이제 일괄 편집 항목/가이드에 따라 그리드에 대한 일괄 편집을 활성화하는 데 중점을 두겠습니다.
샘플을 계속하기 전에 현재 사용 사례를 명확히 하는 것이 좋습니다. 서버에서 페이지 매김이 수행되면 그리드에는 현재 페이지에 대한 데이터만 포함되며 새 행을 추가하면 새로 추가된 행(일괄 편집 사용)이 그리드에 포함된 현재 데이터와 연결됩니다. 따라서 서버가 특정 페이지에 대해 데이터를 반환하지 않으면 그리드의 데이터 소스는 새로 추가된 행으로만 구성되며 그리드는 정의된 페이지 매김 설정(페이지, 페이지당)에 따라 페이지를 매깁니다.
public ngOnInit ( ) {
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data ) => {
this .totalCount = data;
this ._recordOnServer = data;
this ._totalPagesOnServer = Math .floor(this .totalCount / this .perPage);
this .grid1.isLoading = false ;
});
}
typescript
이 사용 사례를 적절하게 처리하려면 일부 사용자 지정 논리를 구현해야 합니다. 먼저, 서버에 있는 총 레코드 수를 알아야 합니다. 이를 고려하여 서버의 총 데이터 페이지 수(this._totalPagesOnServer
참조)를 계산하고 해당 값을 기반으로 사용자 정의 페이지 매김 논리를 구현합니다.
public paginate (page: number ) {
this .grid1.endEdit(true );
if (page > this ._totalPagesOnServer) {
if (this .page !== this ._totalPagesOnServer) {
const skipEl = this ._totalPagesOnServer * this .perPage;
this .remoteService.getData(skipEl, this .perPage);
}
this .page = page - this ._totalPagesOnServer;
this .page = page;
return ;
} else {
this .page = 0 ;
}
this .page = page;
const skip = this .page * this .perPage;
this .remoteService.getData(skip, this .perPage);
}
typescript
paginate 메소드에서 볼 수 있듯이_totalPagesOnServer
값을 기반으로 사용자 정의 페이지 매김 로직이 수행됩니다.
일괄 편집 데모를 통한 원격 페이징
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation, inject } from '@angular/core' ;
import { IgxDialogComponent, IgxGridComponent, Transaction, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxButtonDirective, IgxColumnComponent, IgxCellTemplateDirective } from 'igniteui-angular' ;
import { Observable } from 'rxjs' ;
import { RemotePagingWithBatchEditingService } from '../../services/remotePagingWithBatchEditing.service' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
import { AsyncPipe } from '@angular/common' ;
@Component ({
encapsulation : ViewEncapsulation.None,
providers : [RemotePagingWithBatchEditingService],
selector : 'app-remote-paging-batch-editing' ,
styleUrls : ['./batch-editing-remote-paging.component.scss' ],
templateUrl : './batch-editing-remote-paging.component.html' ,
imports : [IgxGridComponent, IgxPreventDocumentScrollDirective, IgxPaginatorComponent, IgxPaginatorContentDirective, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxButtonDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxDialogComponent, AsyncPipe]
})
export class RemotePagingBatchEditingComponent implements OnInit , AfterViewInit , OnDestroy {
private remoteService = inject(RemotePagingWithBatchEditingService);
@ViewChild ('grid1' , { static : true }) public grid1: IgxGridComponent;
@ViewChild (IgxDialogComponent, { static : true }) public dialog: IgxDialogComponent;
public page = 0 ;
public totalCount = 0 ;
public data: Observable<any []>;
public selectOptions = [5 , 10 , 15 , 25 , 50 ];
public transactionsData: Transaction[] = [];
private _perPage = 10 ;
private _dataLengthSubscriber;
private _recordsOnServer = 0 ;
private _totalPagesOnServer = 0 ;
public get perPage (): number {
return this ._perPage;
}
public set perPage (val: number ) {
this ._perPage = val;
this ._totalPagesOnServer = Math .floor(this ._recordsOnServer / this .perPage);
this .paginate(0 );
}
public ngOnInit ( ) {
this .data = this .remoteService.data$;
this ._dataLengthSubscriber = this .remoteService.getDataLength().subscribe((data ) => {
this .totalCount = data;
this ._recordsOnServer = data;
this ._totalPagesOnServer = Math .floor(this .totalCount / this .perPage);
});
this .remoteService.getData(0 , this .perPage).subscribe(() => {
this .grid1.isLoading = false ;
});
}
public ngOnDestroy ( ) {
if (this ._dataLengthSubscriber) {
this ._dataLengthSubscriber.unsubscribe();
}
}
public ngAfterViewInit ( ) {
this .grid1.isLoading = true ;
}
public paginate (page: number ) {
this .grid1.isLoading = true ;
this .grid1.endEdit(true );
if (page > this ._totalPagesOnServer) {
if (this .page !== this ._totalPagesOnServer) {
const skipEl = this ._totalPagesOnServer * this .perPage;
this .remoteService.getData(skipEl, skipEl + this .perPage);
}
this .grid1.isLoading = false ;
this .grid1.paginator.page = page - this ._totalPagesOnServer;
this .page = page;
return ;
} else if (this .grid1.paginator) {
const newPage = page - this ._totalPagesOnServer > -1 ? page - this ._totalPagesOnServer : 0 ;
this .grid1.paginator.page = newPage;
}
const skip = page * this .perPage;
this .remoteService.getData(skip, skip + this .perPage);
this .page = page;
}
public addRow ( ) {
this .totalCount++;
const newID = this .generateRandomInteger(this .totalCount, this .totalCount * 100 );
this .grid1.addRow({
ID : newID, ProductName : 'Product Name' , QuantityPerUnit : 'Quantity per Unit' ,
SupplierName : 'Supplier Name' , UnitsInStock : 1 , Rating : 1
});
}
public deleteRow (rowID ) {
const isTransaction = !this .grid1.data.some(d => d.ID === rowID);
if (isTransaction) {
this .totalCount--;
}
this .grid1.deleteRow(rowID);
if (isTransaction && this .grid1.dataView.length === 1 ) {
this .paginate(this .page - 1 );
}
}
public generateRandomInteger (start: number , end: number ) {
return Math .floor(Math .random() * (end - start + 1 )) + start;
}
public undo ( ) {
this .grid1.transactions.undo();
this .computeTotalCount();
this .preventDisplayingEmptyPages();
}
public redo ( ) {
this .grid1.transactions.redo();
this .computeTotalCount();
this .preventDisplayingEmptyPages();
}
public openCommitDialog ( ) {
this .transactionsData = this .grid1.transactions.getAggregatedChanges(true );
this .dialog.open();
}
public commit ( ) {
this .grid1.isLoading = true ;
this .dialog.close();
const aggregatedChanges = this .grid1.transactions.getAggregatedChanges(true );
this .remoteService.processBatch(aggregatedChanges).subscribe({
next : (count: number ) => {
this .totalCount = count;
this ._recordsOnServer = count;
this .grid1.transactions.commit(this .grid1.data);
this .preventDisplayingEmptyPages();
},
error : err => {
console .log(err);
},
complete : () => {
this .grid1.isLoading = false ;
}
});
}
public cancel ( ) {
this .dialog.close();
}
public discard ( ) {
this .grid1.transactions.clear();
this .totalCount = this ._recordsOnServer;
this .preventDisplayingEmptyPages();
this .dialog.close();
}
public get hasTransactions (): boolean {
return this .grid1.transactions.getAggregatedChanges(false ).length > 0 ;
}
public stateFormatter (value: string ) {
return JSON .stringify(value);
}
public typeFormatter (value: string ) {
return value.toUpperCase();
}
public classFromType(type : string ): string {
return `transaction--${type .toLowerCase()} ` ;
}
private preventDisplayingEmptyPages ( ) {
this ._totalPagesOnServer = Math .floor(this ._recordsOnServer / this .perPage);
const totalPages = Math .floor(this .totalCount / this .perPage);
if (this .page > 0 &&
(this .page > totalPages ||
(this .page === totalPages &&
this .totalCount % this .perPage === 0 ))) {
this .paginate(totalPages - 1 );
}
}
private computeTotalCount ( ) {
this .totalCount = this ._recordsOnServer + this .grid1.transactions.getAggregatedChanges(true ).filter(rec => rec.type === 'add' ).length;
}
}
ts コピー <div class ="grid__wrapper" >
<igx-grid [igxPreventDocumentScroll ]="true" #grid1 [batchEditing ]="true" [data ]="data | async" width ="100%" height ="580px"
[rowEditable ]="true" [primaryKey ]="'ID'" >
<igx-paginator [perPage ]="perPage" >
<igx-paginator-content >
<igx-paginator #paginator [totalRecords ]="totalCount" [page ]="page" [(perPage )]="perPage"
[selectOptions ]="selectOptions"
(pageChange )="paginate($event)" >
</igx-paginator >
</igx-paginator-content >
</igx-paginator >
<igx-grid-toolbar >
<igx-grid-toolbar-actions >
<button igxButton (click )="addRow()" > Add Row</button >
<button igxButton [disabled ]="!grid1.transactions.canUndo" (click )="undo()" > Undo</button >
<button igxButton [disabled ]="!grid1.transactions.canRedo" (click )="redo()" > Redo</button >
<button igxButton [disabled ]="!hasTransactions" (click )="openCommitDialog()" > Commit</button >
</igx-grid-toolbar-actions >
</igx-grid-toolbar >
<igx-column [pinned ]="true" [filterable ]="false" [editable ]="false" >
<ng-template igxCell let-cell ="cell" let-val >
<button igxButton (click )="deleteRow(cell.id.rowID)" [disabled ]="cell.row.deleted" > Delete</button >
</ng-template >
</igx-column >
<igx-column field ="ID" [editable ]="false" > </igx-column >
<igx-column field ="ProductName" > </igx-column >
<igx-column field ="QuantityPerUnit" > </igx-column >
<igx-column field ="SupplierName" > </igx-column >
<igx-column field ="UnitsInStock" > </igx-column >
<igx-column field ="Rating" > </igx-column >
</igx-grid >
<igx-dialog title ="Submit the following transactions?" >
<igx-grid [igxPreventDocumentScroll ]="true" #dialogGrid [data ]="transactionsData" [rowHeight ]="64" [primaryKey ]="'id'"
width ="600px" height ="300px" [emptyGridMessage ]="'No available transactions'" >
<igx-column field ="id" header ="ID" [dataType ]="'string'" width ="100px" > </igx-column >
<igx-column field ="type" header ="Type" width ="150px" [sortable ]="true" >
</igx-column >
<igx-column field ="newValue" header ="Value" width ="900px" >
<ng-template igxCell let-cell ="cell" let-val >
<span class ="transaction-log" > {{ stateFormatter(val) }}</span >
</ng-template >
</igx-column >
</igx-grid >
<div class ="buttons-wrapper" >
<button igxButton (click )="commit()" > Commit</button >
<button igxButton (click )="discard()" > Discard</button >
<button igxButton (click )="cancel()" > Cancel</button >
</div >
</igx-dialog >
</div >
html コピー .grid__wrapper {
margin : 0 auto;
padding : 16px ;
}
.buttons-row {
display : flex;
flex-direction : row;
justify-content : space-between;
padding : 5px ;
}
.buttons-wrapper {
display : flex;
flex-direction : row;
justify-content : center;
padding : 10px 0 ;
}
.transaction--update , .transaction--delete , .transaction--add {
font-weight : 600 ;
}
.transaction--add {
color : #6b3 ;
}
.transaction--update {
color : #4a71b9 ;
}
.transaction--delete {
color : #ee4920 ;
}
.transaction-log {
word-wrap : none;
}
igx-paginator {
igx-paginator {
padding : 0 !important ;
}
}
scss コピー
알려진 문제 및 제한 사항
그리드에 primaryKey
설정되지 않고 원격 데이터 시나리오가 활성화된 경우(그리드에 표시할 데이터를 검색하기 위해 원격 서버에 대한 페이징, 정렬, 필터링, 스크롤 트리거 요청 시) 행은 데이터 이후 다음 상태를 잃게 됩니다. 요청이 완료되었습니다:
원격 데이터 시나리오에서 그리드에 primaryKey
설정된 경우 rowSelectionChanging.oldSelection
이벤트 인수에는 현재 데이터 보기에 없는 행에 대한 전체 행 데이터 개체가 포함되지 않습니다. 이 경우 rowSelectionChanging.oldSelection
객체에는 primaryKey
필드인 하나의 속성만 포함됩니다. 현재 데이터 보기에 있는 나머지 행의 경우 rowSelectionChanging.oldSelection
전체 행 데이터가 포함됩니다.
API 참조
추가 리소스
우리 커뮤니티는 활동적이며 항상 새로운 아이디어를 환영합니다.