-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/data provider strategies #585
base: master
Are you sure you want to change the base?
Changes from all commits
b44d38c
abdd742
5896dad
1366258
2d6513e
7e88f63
862926b
8e435ac
5c848a8
5410eca
4b77842
99e1bb8
ac56e85
ab5c721
ca9c107
5e06bc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { ChangeDetectionStrategy, Component } from '@angular/core'; | ||
import { GridModel } from 'ng2-qgrid'; | ||
import { Observable } from 'rxjs'; | ||
import { map, switchMap, tap } from 'rxjs/operators'; | ||
import { CacheAlreadyRequestedPageStrategy, CheckNextPageCountStrategy, DataProvider, DataProviderPageServer, DataProviderStrategy, Grid, GridModel, RequestTotalCountOnceStategy } from 'ng2-qgrid'; | ||
import { Observable, of } from 'rxjs'; | ||
import { map } from 'rxjs/operators'; | ||
import { Atom, DataService } from '../data.service'; | ||
|
||
const EXAMPLE_TAGS = [ | ||
|
@@ -12,6 +12,7 @@ const EXAMPLE_TAGS = [ | |
@Component({ | ||
selector: 'example-data-provider', | ||
templateUrl: 'example-data-provider.component.html', | ||
styleUrls: ['example-data-provider.component.scss'], | ||
providers: [DataService], | ||
changeDetection: ChangeDetectionStrategy.OnPush | ||
}) | ||
|
@@ -21,34 +22,44 @@ export class ExampleDataProviderComponent { | |
|
||
page$: Observable<Atom[]>; | ||
|
||
gridModel = this.qgrid.model(); | ||
|
||
private server = new FakeServer(this.dataService); | ||
private dataProvider: DataProvider<Atom> = new DataProvider<Atom>(this.gridModel, [ | ||
new CheckNextPageCountStrategy(this.server), | ||
new RequestTotalCountOnceStategy(this.server), | ||
new CacheAlreadyRequestedPageStrategy(this.server, { pagesToLoad: 1 }), | ||
new ExampleReverseDataStrategy(), | ||
]); | ||
|
||
constructor( | ||
private dataService: DataService, | ||
private qgrid: Grid, | ||
) { } | ||
|
||
onRequestRows(gridModel: GridModel): void { | ||
const server = new FakeServer(this.dataService); | ||
const pager = gridModel.pagination(); | ||
|
||
this.page$ = server.getTotal() | ||
.pipe( | ||
tap(total => gridModel.pagination({ count: total })), | ||
switchMap(() => server.getPage(pager.current, pager.size)), | ||
); | ||
this.page$ = this.dataProvider.getPage(); | ||
} | ||
} | ||
|
||
class FakeServer { | ||
class FakeServer implements DataProviderPageServer<Atom> { | ||
constructor( | ||
private dataService: DataService, | ||
) { } | ||
|
||
getPage(pageNumber: number, pageSize: number): Observable<Atom[]> { | ||
getRecords(from: number, to: number): Observable<Atom[]> { | ||
return this.dataService.getAtoms() | ||
.pipe(map(atoms => atoms.splice(pageNumber * pageSize, pageSize))); | ||
.pipe(map(atoms => atoms.slice(from, to))); | ||
} | ||
|
||
getTotal(): Observable<number> { | ||
return this.dataService.getAtoms() | ||
.pipe(map(atoms => atoms.length)); | ||
} | ||
} | ||
|
||
class ExampleReverseDataStrategy<T> implements DataProviderStrategy<T> { | ||
process(memo: T[]): Observable<T[]> { | ||
return of(memo.slice().reverse()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why we need to do memo slice? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename it please to ExampleReverseDataStrategy There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
.reverse() mutates initial array |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { GridModel } from '@qgrid/ngx'; | ||
import { Observable, of } from 'rxjs'; | ||
import { filter, tap } from 'rxjs/operators'; | ||
import { DataProviderContext } from './data-provider'; | ||
import { DataProviderPageServer } from './data-provider-page-server'; | ||
import { DataProviderStrategy } from './data-provider-strategy'; | ||
|
||
export class CacheAlreadyRequestedPageStrategy<T> implements DataProviderStrategy<T> { | ||
private pageCache: Map<number, T[]> = new Map(); | ||
private pagerSize: number; | ||
|
||
constructor( | ||
private server: Pick<DataProviderPageServer<T>, 'getRecords'>, | ||
private options: { pagesToLoad: number } = { pagesToLoad: 1 }, | ||
) { } | ||
|
||
process(data: T[], { model }: DataProviderContext): Observable<T[]> { | ||
const { current, size } = model.pagination(); | ||
|
||
if (this.options.pagesToLoad) { | ||
this.loadBackgroundPages(this.options.pagesToLoad, model); | ||
} | ||
|
||
if (this.pageCache.has(current)) { | ||
return of(this.pageCache.get(current)); | ||
} | ||
|
||
const shouldRequestData = !data.length; | ||
return (shouldRequestData ? this.server.getRecords(current * size, (current + 1) * size) : of(data)) | ||
.pipe(tap(rows => this.pageCache.set(current, rows))); | ||
} | ||
|
||
invalidate(gridModel: GridModel): void { | ||
const { size } = gridModel.pagination(); | ||
if (this.pagerSize !== size) { | ||
this.pageCache.clear(); | ||
this.pagerSize = size; | ||
} | ||
} | ||
|
||
private loadBackgroundPages(pagesToLoad: number, model: GridModel): void { | ||
const { count, current, size } = model.pagination(); | ||
const fromPage = current + 1; | ||
const toPage = current + pagesToLoad; | ||
const maxPage = Math.floor(count / size); | ||
for (let page = fromPage; page < toPage; page++) { | ||
if (page <= maxPage && !this.pageCache.has(page)) { | ||
this.server.getRecords(page * size, (page + 1) * size) | ||
.pipe(filter(rows => !!rows?.length)) | ||
.subscribe(rows => this.pageCache.set(page, rows)); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { GridModel } from '@qgrid/ngx'; | ||
import { Observable, of } from 'rxjs'; | ||
import { map } from 'rxjs/operators'; | ||
import { DataProviderContext } from './data-provider'; | ||
import { DataProviderPageServer } from './data-provider-page-server'; | ||
import { DataProviderStrategy } from './data-provider-strategy'; | ||
|
||
export class CheckNextPageCountStrategy<T> implements DataProviderStrategy<T> { | ||
private cache: Set<number> = new Set(); | ||
private pagerSize: number; | ||
|
||
constructor( | ||
private server: Pick<DataProviderPageServer<T>, 'getRecords'>, | ||
) { } | ||
|
||
process(data: T[], { model }: DataProviderContext): Observable<T[]> { | ||
const { current } = model.pagination(); | ||
|
||
if (this.cache.has(current)) { | ||
return of(data); | ||
} | ||
|
||
const { count, size } = model.pagination(); | ||
return this.server.getRecords(current * size, (current + 1) * size + 1) | ||
.pipe( | ||
map((next: T[]) => { | ||
if (next?.length > size) { | ||
next.pop(); | ||
} | ||
model.pagination({ count: next.length + (count || 1) }); | ||
this.cache.add(current); | ||
return next; | ||
}), | ||
); | ||
} | ||
|
||
invalidate(gridModel: GridModel): void { | ||
const { size } = gridModel.pagination(); | ||
if (size !== this.pagerSize) { | ||
this.cache.clear(); | ||
gridModel.pagination({ count: 0 }); | ||
this.pagerSize = size; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Observable } from 'rxjs'; | ||
|
||
export interface DataProviderPageServer<T> { | ||
getRecords(from: number, to: number): Observable<T[]>; | ||
getTotal(): Observable<number>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { GridModel } from '@qgrid/ngx'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can u put all strategies directly to the data-provider folder? also remove please index.ts file, we don't use them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
import { Observable } from 'rxjs'; | ||
import { DataProviderContext } from './data-provider'; | ||
|
||
export interface DataProviderStrategy<T> { | ||
process(data: T[], context: DataProviderContext): Observable<T[]>; | ||
invalidate?(gridModel: GridModel): void; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { GridModel } from '@qgrid/ngx'; | ||
import { Observable, of } from 'rxjs'; | ||
import { switchMap } from 'rxjs/operators'; | ||
import { DataProviderStrategy } from './data-provider-strategy'; | ||
|
||
export interface DataProviderContext { | ||
model: GridModel; | ||
} | ||
|
||
export class DataProvider<T> { | ||
|
||
constructor( | ||
private gridModel: GridModel, | ||
private strategies: DataProviderStrategy<T>[], | ||
) { } | ||
|
||
getPage(): Observable<T[]> { | ||
return this.applyStrategies(); | ||
} | ||
|
||
private applyStrategies(data = [], index = 0): Observable<T[]> { | ||
const strategy = this.strategies[index]; | ||
const hasNext = !!this.strategies[index + 1]; | ||
if (!strategy) { | ||
return of(data); | ||
} | ||
|
||
if (typeof strategy.invalidate === 'function') { | ||
strategy.invalidate(this.gridModel); | ||
} | ||
|
||
return strategy.process(data, { model: this.gridModel }) | ||
.pipe(switchMap(x => hasNext ? this.applyStrategies(x, index + 1) : of(x))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Observable, of } from 'rxjs'; | ||
import { switchMap, tap } from 'rxjs/operators'; | ||
import { DataProviderContext } from './data-provider'; | ||
import { DataProviderPageServer } from './data-provider-page-server'; | ||
import { DataProviderStrategy } from './data-provider-strategy'; | ||
|
||
export class RequestTotalCountOnceStategy<T> implements DataProviderStrategy<T> { | ||
private totalCount: number = 0; | ||
|
||
constructor( | ||
private server: Pick<DataProviderPageServer<T>, 'getTotal'>, | ||
) { } | ||
|
||
process(data: T[], { model }: DataProviderContext): Observable<T[]> { | ||
if (this.totalCount > 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what will happen if count will be 0 from the server? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if there was empty array from server wi will try to send that request everytime. Do you think we should remove it? |
||
model.pagination({ count: this.totalCount }); | ||
return of(data); | ||
} | ||
|
||
return this.server.getTotal() | ||
.pipe( | ||
tap(count => { | ||
this.totalCount = count; | ||
model.pagination({ count }); | ||
}), | ||
switchMap(() => of(data)) | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.