Skip to content

Instantly share code, notes, and snippets.

@hakimio
Last active October 19, 2020 12:34
Show Gist options
  • Select an option

  • Save hakimio/3ba0b9f4a186283ee8f55bec113a2492 to your computer and use it in GitHub Desktop.

Select an option

Save hakimio/3ba0b9f4a186283ee8f55bec113a2492 to your computer and use it in GitHub Desktop.
DevExtreme usage with Akita state management
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);
}
}
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;
}
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()
});
}
}
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();
}
}
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);
}
}
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);
}
}
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