Last active
October 19, 2020 12:34
-
-
Save hakimio/3ba0b9f4a186283ee8f55bec113a2492 to your computer and use it in GitHub Desktop.
DevExtreme usage with Akita state management
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {EntityState, getEntityType, getIDType} from '@datorama/akita'; | |
| import {HttpMethod, NgEntityService} from '@datorama/akita-ng-entity-service'; | |
| import {HttpGetConfig} from '@datorama/akita-ng-entity-service/lib/types'; | |
| import {Observable, throwError} from 'rxjs'; | |
| import {DxLoadOptions} from '../query-builder/query-builder.model'; | |
| import {QueryBuilder} from '../query-builder/query-builder.service'; | |
| import {HttpErrorResponse} from '@angular/common/http'; | |
| import {HttpGetResponse, LoadResult} from './base-store.model'; | |
| import {map, tap} from 'rxjs/operators'; | |
| import {DxStoreChange, DxStoreChangeType} from '../dx-store-manager/dx-store-manager.model'; | |
| import {DxStoreManager} from '../dx-store-manager/dx-store-manager'; | |
| export abstract class BaseStoreService<S extends EntityState = any, EntityType = getEntityType<S>> extends NgEntityService<S> { | |
| public static readonly MAX_RESULTS_LIMIT = 1e3; | |
| load<T = EntityType>(loadOptions: DxLoadOptions = {}): Observable<LoadResult<T>> { | |
| let paginatedResult = true; | |
| if (!loadOptions.take) { | |
| loadOptions.take = BaseStoreService.MAX_RESULTS_LIMIT; | |
| paginatedResult = false; | |
| } | |
| const params = QueryBuilder.build(loadOptions), | |
| config: HttpGetConfig = { | |
| params, | |
| skipWrite: true | |
| }; | |
| return super.get<HttpGetResponse>(config) | |
| .pipe( | |
| map( | |
| response => { | |
| // TODO: create utility for cloning stuff | |
| const entities = response.data.map(item => ({...item})); | |
| this.store.upsertMany(entities); | |
| return !paginatedResult ? response.data : { | |
| data: response.data, | |
| totalCount: response.total | |
| }; | |
| } | |
| ) | |
| ); | |
| } | |
| add<T>(entity: getEntityType<S>): Observable<T> { | |
| return super.add<T>(entity) | |
| .pipe( | |
| tap( | |
| data => this.pushChangeToDxStore({ | |
| type: DxStoreChangeType.Insert, | |
| data, | |
| index: 0 | |
| }) | |
| ) | |
| ); | |
| } | |
| update<T>(id: getIDType<S>, entity: Partial<getEntityType<S>>): Observable<T> { | |
| return super.update<T>(id, entity) | |
| .pipe( | |
| tap( | |
| data => this.pushChangeToDxStore({ | |
| type: DxStoreChangeType.Update, | |
| data, | |
| key: id | |
| }) | |
| ) | |
| ); | |
| } | |
| delete<T>(id: getIDType<S>): Observable<T> { | |
| return super.delete<T>(id) | |
| .pipe( | |
| tap( | |
| () => this.pushChangeToDxStore({ | |
| type: DxStoreChangeType.Remove, | |
| key: id | |
| }) | |
| ) | |
| ); | |
| } | |
| private pushChangeToDxStore(change: DxStoreChange) { | |
| const store = DxStoreManager.getOrCreateStore(<any>this.constructor); | |
| store.push([change]); | |
| } | |
| protected handleError(method: HttpMethod, error: HttpErrorResponse): Observable<never> { | |
| const message = error.error?.message ?? 'Server error'; | |
| return throwError(message); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import CustomStore, {CustomStoreOptions} from 'devextreme/data/custom_store'; | |
| import {DxLoadOptions} from '../query-builder/query-builder.model'; | |
| import {LoadResult} from '../base-store/base-store.model'; | |
| export class DxStore<T> extends CustomStore { | |
| constructor(options: DxStoreOptions<T>) { | |
| super(options); | |
| } | |
| } | |
| export interface DxStoreOptions<T> extends CustomStoreOptions { | |
| load: (loadOptions: DxLoadOptions) => Promise<LoadResult<T>>; | |
| byKey: (id: number) => Promise<T>; | |
| insert: (data: Partial<T>) => Promise<T>; | |
| update: (id: number, data: Partial<T>) => Promise<T>; | |
| remove: (id: number) => Promise<void>; | |
| } | |
| export enum DxStoreChangeType { | |
| Insert = 'insert', | |
| Update = 'update', | |
| Remove = 'remove' | |
| } | |
| export interface DxStoreChange { | |
| type: DxStoreChangeType; | |
| data?: any; | |
| key?: number; | |
| index?: number; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {BaseStoreService} from '..'; | |
| import {DxStore} from './dx-store-manager.model'; | |
| import {memo} from 'helpful-decorators'; | |
| import {Type} from '@angular/core'; | |
| import {AppInjector} from '../..'; | |
| export class DxStoreManager { | |
| @memo() | |
| static getOrCreateStore<T, S extends BaseStoreService = BaseStoreService>(storeClass: Type<S>): DxStore<T> { | |
| const storeService = AppInjector.getService(storeClass); | |
| return new DxStore<T>({ | |
| key: 'id', | |
| useDefaultSearch: true, | |
| load: loadOptions => storeService.load<T>(loadOptions).toPromise(), | |
| byKey: id => storeService.get<T>(id).toPromise(), | |
| insert: data => storeService.add<T>(data).toPromise(), | |
| update: (id, data) => storeService.update<T>(id, data).toPromise(), | |
| remove: id => storeService.delete<void>(id).toPromise() | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Component, OnInit} from '@angular/core'; | |
| import DataSource from 'devextreme/data/data_source'; | |
| import {DxStoreManager} from '../../shared/services/store/dx-store-manager/dx-store-manager'; | |
| import {Ticket, TicketStatusFilter} from './state/ticket.model'; | |
| import {TicketsService} from './state/tickets.service'; | |
| import {TicketsQuery} from './state/tickets.query'; | |
| import {DxFilter, DxFilterExpression} from '../../shared/services/store/query-builder/query-builder.model'; | |
| import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; | |
| import {ProjectsQuery} from '../projects/state/projects.query'; | |
| import {combineQueries} from '@datorama/akita'; | |
| @UntilDestroy() | |
| @Component({ | |
| selector: 'dxa-tickets', | |
| templateUrl: './tickets.component.html', | |
| styleUrls: ['./tickets.component.scss'] | |
| }) | |
| export class TicketsComponent implements OnInit { | |
| ticketDS: DataSource; | |
| constructor( | |
| private readonly ticketsQuery: TicketsQuery, | |
| private readonly projectsQuery: ProjectsQuery | |
| ) {} | |
| ngOnInit(): void { | |
| this.initTicketDataSource(); | |
| this.loadTicketStatus(); | |
| this.subscribeToFilterUpdates(); | |
| } | |
| initTicketDataSource() { | |
| const ticketStore = DxStoreManager.getOrCreateStore<Ticket>(TicketsService); | |
| this.ticketDS = new DataSource({ | |
| store: ticketStore | |
| }); | |
| } | |
| subscribeToFilterUpdates() { | |
| combineQueries([ | |
| this.ticketsQuery.statusFilter$, | |
| this.projectsQuery.selectedProjectId$ | |
| ]) | |
| .pipe(untilDestroyed(this)) | |
| .subscribe( | |
| ([statusFilter, projectId]) => this.updateFilters(statusFilter, projectId) | |
| ); | |
| } | |
| updateFilters(statusFilter: TicketStatusFilter, projectId: number) { | |
| const dxStatusFilter: DxFilter = ['ticketStatusId', '=', statusFilter], | |
| dxProjectFilter: DxFilter = ['projectId', '=', projectId], | |
| anyStatus = statusFilter === TicketStatusFilter.All, | |
| combinedFilter: DxFilterExpression = anyStatus ? dxProjectFilter : [dxStatusFilter, dxProjectFilter]; | |
| this.ticketDS.filter(combinedFilter); | |
| this.ticketDS.load(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Injectable} from '@angular/core'; | |
| import {QueryEntity} from '@datorama/akita'; | |
| import {TicketsStore, TicketsState} from './tickets.store'; | |
| import {switchMap} from 'rxjs/operators'; | |
| @Injectable({ | |
| providedIn: 'root' | |
| }) | |
| export class TicketsQuery extends QueryEntity<TicketsState> { | |
| focusedTicket$ = this.select('focusedId') | |
| .pipe( | |
| switchMap(id => this.selectEntity(id)) | |
| ); | |
| focusedTicketId$ = this.select('focusedId'); | |
| searchText$ = this.select(state => state.ui.searchText); | |
| statusFilter$ = this.select(state => state.ui.statusFilter); | |
| constructor( | |
| protected store: TicketsStore | |
| ) { | |
| super(store); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Injectable} from '@angular/core'; | |
| import {TicketsStore, TicketsState} from './tickets.store'; | |
| import {BaseStoreService} from '../../../shared/services/store'; | |
| @Injectable({ | |
| providedIn: 'root' | |
| }) | |
| export class TicketsService extends BaseStoreService<TicketsState> { | |
| constructor( | |
| protected store: TicketsStore | |
| ) { | |
| super(store); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Injectable} from '@angular/core'; | |
| import {EntityState, EntityStore, StoreConfig} from '@datorama/akita'; | |
| import {Ticket, TicketStatusFilter} from './ticket.model'; | |
| export interface TicketsState extends EntityState<Ticket> { | |
| focusedId: number; | |
| ui: TicketsUiState; | |
| } | |
| export interface TicketsUiState { | |
| searchText: string; | |
| statusFilter: TicketStatusFilter; | |
| } | |
| const initialState: Partial<TicketsState> = { | |
| ui: { | |
| searchText: '', | |
| statusFilter: TicketStatusFilter.Open | |
| } | |
| }; | |
| @Injectable({ | |
| providedIn: 'root' | |
| }) | |
| @StoreConfig({ | |
| name: 'tickets' | |
| }) | |
| export class TicketsStore extends EntityStore<TicketsState> { | |
| constructor() { | |
| super(initialState); | |
| } | |
| updateSearchText(text: string) { | |
| this.updateUiProperty('searchText', text); | |
| } | |
| updateStatusFilter(statusFilter: TicketStatusFilter) { | |
| this.updateUiProperty('statusFilter', statusFilter); | |
| } | |
| private updateUiProperty(property: keyof TicketsUiState, value: any) { | |
| this.update(state => ({ | |
| ui: { | |
| ...state.ui, | |
| [property]: value | |
| } | |
| }) | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment