diff --git a/src/lib/filtering/filter.service.ts b/src/lib/filtering/filter.service.ts index a95aa7a..44a5679 100644 --- a/src/lib/filtering/filter.service.ts +++ b/src/lib/filtering/filter.service.ts @@ -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 { return this.filterGroups$.pipe( get(group => group.slug === slug), - shareReplay(1), + shareLast(), ); } diff --git a/src/lib/filtering/popup-filter/popup-filter.component.ts b/src/lib/filtering/popup-filter/popup-filter.component.ts index d13c075..f3d086a 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.ts +++ b/src/lib/filtering/popup-filter/popup-filter.component.ts @@ -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(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 { 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$; diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts index 775bc9f..c90035e 100644 --- a/src/lib/listing/listing-component.directive.ts +++ b/src/lib/listing/listing-component.directive.ts @@ -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 extends AutoUnsubscr private get _sortedDisplayedEntities$(): Observable { 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 { return combineLatest([this.entitiesService.allLength$, this.listingService.displayedLength$]).pipe( map(([hasEntities, hasDisplayedEntities]) => !!hasEntities && !hasDisplayedEntities), - shareReplay(1), - distinctUntilChanged(), + shareDistinctLast(), ); } private get _noContent$(): Observable { return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe( map(([noMatch, noData]) => noMatch || noData), - shareReplay(1), - distinctUntilChanged(), + shareDistinctLast(), ); } diff --git a/src/lib/listing/page-header/page-header.component.ts b/src/lib/listing/page-header/page-header.component.ts index 28c211a..9f395c5 100644 --- a/src/lib/listing/page-header/page-header.component.ts +++ b/src/lib/listing/page-header/page-header.component.ts @@ -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 { @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) {} diff --git a/src/lib/listing/scroll-button/scroll-button.component.ts b/src/lib/listing/scroll-button/scroll-button.component.ts index ca614f4..22a3b05 100644 --- a/src/lib/listing/scroll-button/scroll-button.component.ts +++ b/src/lib/listing/scroll-button/scroll-button.component.ts @@ -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 { diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index e070f0b..345adc7 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -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 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 extends GenericService< private get _noData$(): Observable { return this.allLength$.pipe( map(length => length === 0), - distinctUntilChanged(), - shareReplay(1), + shareDistinctLast(), ); } loadAll(modelPath = this._defaultModelPath, queryParams?: List): Observable { 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)), ); } diff --git a/src/lib/listing/services/listing.service.ts b/src/lib/listing/services/listing.service.ts index 2b38dae..545692a 100644 --- a/src/lib/listing/services/listing.service.ts +++ b/src/lib/listing/services/listing.service.ts @@ -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 extends AutoUnsubscribe { +export class ListingService { readonly displayed$: Observable; readonly displayedLength$: Observable; readonly areAllSelected$: Observable; @@ -25,25 +25,16 @@ export class ListingService extends AutoUnsubscribe { private readonly _searchService: SearchService, private readonly _entitiesService: EntitiesService, ) { - 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 extends AutoUnsubscribe { map(entities => this._searchService.searchIn(entities)), tap(displayed => { this._displayed = displayed; + this._updateSelection(); }), - distinctUntilChanged(), - shareReplay(1), + shareLast(), ); } private get _areAllSelected$(): Observable { return combineLatest([this.displayedLength$, this.selectedLength$]).pipe( map(([displayedLength, selectedLength]) => !!displayedLength && displayedLength === selectedLength), - distinctUntilChanged(), - shareReplay(1), + shareDistinctLast(), ); } private get _areSomeSelected$(): Observable { return this.selectedLength$.pipe( map(length => !!length), - distinctUntilChanged(), - shareReplay(1), + shareDistinctLast(), ); } private get _notAllSelected$(): Observable { return combineLatest([this.areAllSelected$, this.areSomeSelected$]).pipe( map(([allAreSelected, someAreSelected]) => !allAreSelected && someAreSelected), - distinctUntilChanged(), - shareReplay(1), + shareDistinctLast(), ); } diff --git a/src/lib/search/search.service.ts b/src/lib/search/search.service.ts index 79a6cf3..c232c0e 100644 --- a/src/lib/search/search.service.ts +++ b/src/lib/search/search.service.ts @@ -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 { 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(); diff --git a/src/lib/sorting/sorting.service.ts b/src/lib/sorting/sorting.service.ts index 987a301..7071699 100644 --- a/src/lib/sorting/sorting.service.ts +++ b/src/lib/sorting/sorting.service.ts @@ -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 { @@ -13,7 +12,7 @@ export class SortingService { column: 'searchKey', order: SortingOrders.asc, }); - readonly sortingOption$ = this._sortingOption$.asObservable().pipe(distinctUntilChanged(), shareReplay(1)); + readonly sortingOption$ = this._sortingOption$.asObservable().pipe(shareDistinctLast()); get sortingOption(): SortingOption | undefined { return this._sortingOption$.getValue(); diff --git a/src/lib/utils/operators.ts b/src/lib/utils/operators.ts index 16292d1..4835fce 100644 --- a/src/lib/utils/operators.ts +++ b/src/lib/utils/operators.ts @@ -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(predicate: (value: T, index: number) => boolean): OperatorFunction { return map(entities => entities.find(predicate)); @@ -21,5 +21,9 @@ export function shareLast(values = 1): MonoTypeOperatorFunction { return shareReplay({ bufferSize: values, refCount: true }); } +export function shareDistinctLast(values = 1): UnaryFunction, Observable> { + return pipe(distinctUntilChanged(), shareLast(values)); +} + export const toLengthValue = (entities: unknown[]): number => entities?.length ?? 0; export const getLength = pipe(map(toLengthValue), distinctUntilChanged());