Skip to content

Commit

Permalink
feat(stream): introduce concept of render strategies
Browse files Browse the repository at this point in the history
* `DefaultRenderStrategy`
* `ThrottleRenderStrategy`
* `DebounceRenderStrategy`
  • Loading branch information
michaelbe812 committed Apr 4, 2023
1 parent ebe638f commit d413113
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 8 deletions.
41 changes: 41 additions & 0 deletions libs/stream/src/lib/render-strategies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface RenderStrategy {
type: 'default' | 'viewport' | 'throttle' | 'debounce';
}

export interface DefaultRenderStategy extends RenderStrategy {
type: 'default';
}

export interface ViewportRenderStrategy extends RenderStrategy {
type: 'viewport';
rootMargin?: string;
threshold?: number | number[];
}

export interface ThrottleRenderStrategy extends RenderStrategy {
type: 'throttle';
throttleInMs: number;
}

export interface DebounceRenderStrategy extends RenderStrategy {
type: 'debounce';
debounceInMs: number;
}

export type RenderStrategies = DefaultRenderStategy | ViewportRenderStrategy | ThrottleRenderStrategy;

export function isDefaultRenderStrategy(strategy: RenderStrategy): strategy is DefaultRenderStategy {
return strategy.type === 'default';
}

export function isViewportRenderStrategy(strategy: RenderStrategy): strategy is ViewportRenderStrategy {
return strategy.type === 'viewport';
}

export function isThrottleRenderStrategy(strategy: RenderStrategy): strategy is ThrottleRenderStrategy {
return strategy.type === 'throttle';
}

export function isDebounceRenderStrategy(strategy: RenderStrategy): strategy is DebounceRenderStrategy {
return strategy.type === 'debounce';
}
94 changes: 86 additions & 8 deletions libs/stream/src/lib/stream.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,33 @@ import {
ViewContainerRef,
} from '@angular/core';
import {
BehaviorSubject,
debounceTime,
distinctUntilChanged,
filter,
map,
mergeAll,
Observable,
of,
pipe,
ReplaySubject,
startWith,
Subject,
Subscription,
switchMap,
throttleTime,
Unsubscribable,
withLatestFrom,
} from 'rxjs';

import {STREAM_DIR_CONFIG, STREAM_DIR_CONTEXT, StreamDirectiveConfig} from './stream-directive-config';
import {
isDebounceRenderStrategy,
isThrottleRenderStrategy,
isViewportRenderStrategy,
RenderStrategies,
} from './render-strategies';
import {coerceObservable} from './util/coerce-observable';

export interface StreamDirectiveContext<T> {
$implicit: T | null;
Expand All @@ -47,6 +61,7 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
private refreshEffect$$ = new ReplaySubject<Subject<any>>(1);
private loadingTemplate$$ = new ReplaySubject<TemplateRef<StreamDirectiveContext<T>>>(1);
private renderCallback$$: ReplaySubject<RenderContext<T>> | undefined;
private renderStrategy$$ = new BehaviorSubject<Observable<RenderStrategies>>(of({ type: 'default' }));

private detach = true;

Expand All @@ -73,6 +88,12 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
}
}

@Input() set streamRenderStrategy(strategy: RenderStrategies | Observable<RenderStrategies>) {
if (strategy) {
this.renderStrategy$$.next(coerceObservable(strategy));
}
}

@Input() streamErrorTemplate: TemplateRef<StreamDirectiveContext<T>> | undefined;
@Input() streamCompleteTemplate: TemplateRef<StreamDirectiveContext<T>> | undefined;
@Input() streamKeepValueOnLoading = false;
Expand All @@ -89,6 +110,27 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
loading: false,
};

readonly renderStrategyOperator$ = this.setupOperator$(this.renderStrategy$$);
readonly source$ = this.source$$.pipe(distinctUntilChanged());

readonly sourceWithOperator$ = this.renderStrategyOperator$.pipe(
withLatestFrom(this.source$),
/**
* unsubscribe from previous source and subscribe to new source
* apply operator from renderStrategy.
*
* when unsubscribe from previous source, the last value will be lost.
* We can fix this by providing context also via observable and
* withLatestFrom it here.
*/
switchMap(([o, source$]) => {
return source$.pipe(
o
//takeUntil(this.renderStrategyOperator$.pipe(skip(1)))
);
})
);

static ngTemplateContextGuard<T>(
directive: StreamDirective<T>,
context: unknown
Expand Down Expand Up @@ -126,17 +168,20 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
}

this.embeddedView.detectChanges();
this.renderCallback$$?.next({renderCycle: 'before-next', value: this.context.$implicit, error: this.context.error});
this.renderCallback$$?.next({
renderCycle: 'before-next',
value: this.context.$implicit,
error: this.context.error,
});
});
this.subscription = this.source$$

this.subscription = this.sourceWithOperator$
.pipe(
distinctUntilChanged(),
mergeAll(),
distinctUntilChanged(),
filter((v) => v !== undefined)
)
.subscribe({
next: (v) => {
next: (v: any) => {
this.context.$implicit = v;
this.context.stream = v;
this.context.loading = false;
Expand All @@ -145,7 +190,11 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
this.embeddedView = this.viewContainerRef.createEmbeddedView(this.templateRef, this.context);

this.embeddedView.detectChanges();
this.renderCallback$$?.next({renderCycle: 'next', value: this.context.$implicit, error: this.context.error})
this.renderCallback$$?.next({
renderCycle: 'next',
value: this.context.$implicit,
error: this.context.error,
});
},
error: (err) => {
this.context.error = err;
Expand All @@ -162,7 +211,11 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
}

this.embeddedView.detectChanges();
this.renderCallback$$?.next({renderCycle: 'error', value: this.context.$implicit, error: this.context.error})
this.renderCallback$$?.next({
renderCycle: 'error',
value: this.context.$implicit,
error: this.context.error,
});
},
complete: () => {
this.context.completed = true;
Expand All @@ -179,7 +232,11 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
}

this.embeddedView.detectChanges();
this.renderCallback$$?.next({renderCycle: 'complete', value: this.context.$implicit, error: this.context.error})
this.renderCallback$$?.next({
renderCycle: 'complete',
value: this.context.$implicit,
error: this.context.error,
});
},
});
}
Expand Down Expand Up @@ -212,4 +269,25 @@ export class StreamDirective<T> implements OnInit, OnDestroy {
this.embeddedView.detach();
}
}

private setupOperator$(renderStrategy$$: BehaviorSubject<Observable<RenderStrategies>>) {
return renderStrategy$$.pipe(
mergeAll(),
distinctUntilChanged(),
filter((strategy) => !isViewportRenderStrategy(strategy)),
map((strategy) => {
if (isThrottleRenderStrategy(strategy)) {
return of(throttleTime(strategy.throttleInMs));
}

if (isDebounceRenderStrategy(strategy)) {
// @ts-ignore todo fix typing issue
return of(debounceTime(strategy.debounceInMs));
}

return of(pipe());
}),
mergeAll()
);
}
}

0 comments on commit d413113

Please sign in to comment.