use refCount for shareReplay & add shareDistinctLast operator

This commit is contained in:
Dan Percic 2021-11-06 15:54:23 +02:00
parent e1ce89e38d
commit 712178ea34
10 changed files with 53 additions and 72 deletions

View File

@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { map, startWith, switchMapTo } from 'rxjs/operators';
import { processFilters, toFlatFilters } from './filter-utils';
import { IFilterGroup } from './models/filter-group.model';
import { INestedFilter } from './models/nested-filter.model';
import { get } from '../utils';
import { get, shareDistinctLast, shareLast } from '../utils';
import { NestedFilter } from './models/nested-filter';
import { Filter } from './models/filter';
@ -16,11 +16,7 @@ export class FilterService {
private readonly _refresh$ = new Subject();
constructor() {
this.filterGroups$ = this._refresh$.pipe(
startWith(''),
switchMap(() => this._filterGroups$.asObservable()),
shareReplay(1),
);
this.filterGroups$ = this._refresh$.pipe(startWith(''), switchMapTo(this._filterGroups$.asObservable()), shareLast());
this.showResetFilters$ = this._showResetFilters$;
}
@ -33,8 +29,7 @@ export class FilterService {
return this.filterGroups$.pipe(
map(toFlatFilters),
map(f => !!f.find(el => el.checked)),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
@ -100,7 +95,7 @@ export class FilterService {
getGroup$(slug: string): Observable<IFilterGroup | undefined> {
return this.filterGroups$.pipe(
get(group => group.slug === slug),
shareReplay(1),
shareLast(),
);
}

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs';
import { delay, distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { any } from '../../utils';
import { delay, map } from 'rxjs/operators';
import { any, shareDistinctLast, shareLast } from '../../utils';
import { handleCheckedValue } from '../filter-utils';
import { FilterService } from '../filter.service';
import { IFilterGroup } from '../models/filter-group.model';
@ -13,8 +13,7 @@ import { Filter, IFilter } from '..';
const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length;
const atLeastOneIsExpandable = pipe(
map<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
@Component({
@ -55,15 +54,14 @@ export class PopupFilterComponent implements OnInit {
return combineLatest([this.primaryFilterGroup$, this.secondaryFilterGroup$]).pipe(
map(([primary, secondary]) => [...(primary?.filters || []), ...(secondary?.filters || [])]),
any(f => f.checked || !!f.indeterminate),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
private get _primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
map(([group]) => this.searchService.searchIn(group?.filters ?? [])),
shareReplay(1),
shareLast(),
);
}
@ -71,15 +69,14 @@ export class PopupFilterComponent implements OnInit {
return combineLatest([this.primaryFilterGroup$, this.primaryFilters$, this.searchService.valueChanges$]).pipe(
map(([group, filters, value]) => [!!group?.filterceptionPlaceholder, filters?.length === 0, value === '']),
map(([hasFilterSearch, noFilters, searchIsEmpty]) => noFilters && (!hasFilterSearch || (hasFilterSearch && searchIsEmpty))),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
ngOnInit(): void {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareReplay(1));
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
this.primaryFilters$ = this._primaryFilters$;
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareReplay(1));
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
this.hasActiveFilters$ = this._hasActiveFilters$;
this.primaryFiltersDisabled$ = this._primaryFiltersDisabled$;

View File

@ -1,9 +1,9 @@
import { Directive, Injector, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
import { map, switchMapTo } from 'rxjs/operators';
import { FilterService } from '../filtering';
import { SortingService } from '../sorting';
import { AutoUnsubscribe } from '../utils';
import { AutoUnsubscribe, shareDistinctLast } from '../utils';
import { SearchService } from '../search';
import { EntitiesService, ListingService } from './services';
import { IListable, ListingMode, ListingModes, TableColumnConfig } from './models';
@ -51,23 +51,21 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
private get _sortedDisplayedEntities$(): Observable<readonly T[]> {
const sort = (entities: T[]) => this.sortingService.defaultSort(entities);
const sortedEntities = () => this.listingService.displayed$.pipe(map(sort));
return this.sortingService.sortingOption$.pipe(switchMap(sortedEntities), distinctUntilChanged(), shareReplay(1));
const sortedEntities$ = this.listingService.displayed$.pipe(map(sort));
return this.sortingService.sortingOption$.pipe(switchMapTo(sortedEntities$), shareDistinctLast());
}
private get _noMatch$(): Observable<boolean> {
return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities),
shareReplay(1),
distinctUntilChanged(),
shareDistinctLast(),
);
}
private get _noContent$(): Observable<boolean> {
return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe(
map(([noMatch, noData]) => noMatch || noData),
shareReplay(1),
distinctUntilChanged(),
shareDistinctLast(),
);
}

View File

@ -6,6 +6,7 @@ import { IListable } from '../models';
import { IconButtonTypes } from '../../buttons';
import { SearchService } from '../../search';
import { FilterService } from '../../filtering';
import { filterEach } from '../../utils';
@Component({
selector: 'iqser-page-header',
@ -27,7 +28,7 @@ export class PageHeaderComponent<T extends IListable> {
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
@Output() readonly closeAction = new EventEmitter();
readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
readonly filters$ = this.filterService?.filterGroups$.pipe(filterEach(f => !!f.icon));
readonly showResetFilters$ = this._showResetFilters$;
constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {}

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { delay, distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { delay, map, startWith } from 'rxjs/operators';
import { combineLatest, fromEvent, Observable } from 'rxjs';
import { Required } from '../../utils';
import { Required, shareDistinctLast } from '../../utils';
const ButtonTypes = {
top: 'top',
@ -40,10 +40,10 @@ export class ScrollButtonComponent implements OnInit {
const rangeChange$ = this.scrollViewport.renderedRangeStream.pipe(startWith(null));
/** Delay so that we can wait for items to be rendered in viewport and get correct values */
const scroll$ = combineLatest([scrolled$, resized$, rangeChange$]).pipe(delay(0));
const scroll$ = combineLatest([scrolled$, resized$, rangeChange$]).pipe(delay(0), shareDistinctLast());
this.showScrollUp$ = scroll$.pipe(map(showScrollUp), distinctUntilChanged());
this.showScrollDown$ = scroll$.pipe(map(showScrollDown), distinctUntilChanged());
this.showScrollUp$ = scroll$.pipe(map(showScrollUp));
this.showScrollDown$ = scroll$.pipe(map(showScrollDown));
}
scroll(type: ButtonType): void {

View File

@ -1,9 +1,9 @@
import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, tap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { IListable } from '../models';
import { GenericService, QueryParam } from '../../services';
import { getLength, List } from '../../utils';
import { getLength, List, mapEach, shareDistinctLast } from '../../utils';
/**
* This should be removed when refactoring is done
@ -30,8 +30,8 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
@Optional() @Inject(ENTITY_PATH) protected readonly _defaultModelPath = '',
) {
super(_injector, _defaultModelPath);
this.all$ = this._all$.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
this.allLength$ = this._all$.pipe(getLength, distinctUntilChanged(), shareReplay(1));
this.all$ = this._all$.asObservable().pipe(shareDistinctLast());
this.allLength$ = this._all$.pipe(getLength, shareDistinctLast());
this.noData$ = this._noData$;
}
@ -42,14 +42,13 @@ export class EntitiesService<E extends IListable, I = E> extends GenericService<
private get _noData$(): Observable<boolean> {
return this.allLength$.pipe(
map(length => length === 0),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
loadAll(modelPath = this._defaultModelPath, queryParams?: List<QueryParam>): Observable<E[]> {
return this.getAll(modelPath, queryParams).pipe(
map((entities: I[]) => entities.map(entity => new this._entityClass(entity))),
mapEach(entity => new this._entityClass(entity)),
tap((entities: E[]) => this.setEntities(entities)),
);
}

View File

@ -1,14 +1,14 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, tap } from 'rxjs/operators';
import { map, mapTo, tap } from 'rxjs/operators';
import { FilterService, getFilteredEntities } from '../../filtering';
import { SearchService } from '../../search';
import { IListable } from '../models';
import { EntitiesService } from './entities.service';
import { AutoUnsubscribe, getLength } from '../../utils';
import { getLength, shareDistinctLast, shareLast } from '../../utils';
@Injectable()
export class ListingService<E extends IListable> extends AutoUnsubscribe {
export class ListingService<E extends IListable> {
readonly displayed$: Observable<E[]>;
readonly displayedLength$: Observable<number>;
readonly areAllSelected$: Observable<boolean>;
@ -25,25 +25,16 @@ export class ListingService<E extends IListable> extends AutoUnsubscribe {
private readonly _searchService: SearchService<E>,
private readonly _entitiesService: EntitiesService<E>,
) {
super();
this.displayed$ = this._getDisplayed$;
this.displayedLength$ = this.displayed$.pipe(getLength, distinctUntilChanged(), shareReplay(1));
this.displayedLength$ = this.displayed$.pipe(getLength, shareDistinctLast());
this.selected$ = this._selected$.asObservable().pipe(shareReplay(1));
this.selectedEntities$ = this._selected$.asObservable().pipe(
map(() => this.selected),
shareReplay(1),
);
this.selectedLength$ = this._selected$.pipe(getLength, distinctUntilChanged(), shareReplay(1));
this.selected$ = this._selected$.asObservable().pipe(shareDistinctLast());
this.selectedEntities$ = this._selected$.asObservable().pipe(mapTo(this.selected), shareLast());
this.selectedLength$ = this._selected$.pipe(getLength, shareDistinctLast());
this.areAllSelected$ = this._areAllSelected$;
this.areSomeSelected$ = this._areSomeSelected$;
this.notAllSelected$ = this._notAllSelected$;
this.addSubscription = this.displayed$.subscribe(() => {
this._updateSelection();
});
}
get selected(): E[] {
@ -64,33 +55,30 @@ export class ListingService<E extends IListable> extends AutoUnsubscribe {
map(entities => this._searchService.searchIn(entities)),
tap(displayed => {
this._displayed = displayed;
this._updateSelection();
}),
distinctUntilChanged(),
shareReplay(1),
shareLast(),
);
}
private get _areAllSelected$(): Observable<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
private get _areSomeSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(length => !!length),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}
private get _notAllSelected$(): Observable<boolean> {
return combineLatest([this.areAllSelected$, this.areSomeSelected$]).pipe(
map(([allAreSelected, someAreSelected]) => !allAreSelected && someAreSelected),
distinctUntilChanged(),
shareReplay(1),
shareDistinctLast(),
);
}

View File

@ -1,13 +1,13 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { IListable } from '../listing';
import { distinctUntilChanged, shareReplay } from 'rxjs/operators';
import { shareDistinctLast } from '../utils';
@Injectable()
export class SearchService<T extends IListable> {
skip = false;
private readonly _query$ = new BehaviorSubject('');
readonly valueChanges$ = this._query$.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
readonly valueChanges$ = this._query$.asObservable().pipe(shareDistinctLast());
get searchValue(): string {
return this._query$.getValue();

View File

@ -3,9 +3,8 @@ import { orderBy } from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { SortingOption } from './models/sorting-option.model';
import { SortingOrder, SortingOrders } from './models/sorting-order.type';
import { KeysOf } from '../utils';
import { KeysOf, shareDistinctLast } from '../utils';
import { IListable } from '../listing';
import { distinctUntilChanged, shareReplay } from 'rxjs/operators';
@Injectable()
export class SortingService<T extends IListable> {
@ -13,7 +12,7 @@ export class SortingService<T extends IListable> {
column: 'searchKey',
order: SortingOrders.asc,
});
readonly sortingOption$ = this._sortingOption$.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
readonly sortingOption$ = this._sortingOption$.asObservable().pipe(shareDistinctLast());
get sortingOption(): SortingOption<T> | undefined {
return this._sortingOption$.getValue();

View File

@ -1,5 +1,5 @@
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { MonoTypeOperatorFunction, OperatorFunction, pipe } from 'rxjs';
import { MonoTypeOperatorFunction, Observable, OperatorFunction, pipe, UnaryFunction } from 'rxjs';
export function get<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<readonly T[], T | undefined> {
return map(entities => entities.find(predicate));
@ -21,5 +21,9 @@ export function shareLast<T>(values = 1): MonoTypeOperatorFunction<T> {
return shareReplay<T>({ bufferSize: values, refCount: true });
}
export function shareDistinctLast<T>(values = 1): UnaryFunction<Observable<T>, Observable<T>> {
return pipe(distinctUntilChanged(), shareLast(values));
}
export const toLengthValue = (entities: unknown[]): number => entities?.length ?? 0;
export const getLength = pipe(map(toLengthValue), distinctUntilChanged());