Take once only when a boolean changes from false to true - rxjs

I have multiple boolean properties which are false by default, but that may only be set to true at the start of the application. Should the property become true, I should take action once and then unsubscribe.
Here is a simple representation in a class. The default value of source is false, and an on going interval will set it to true. I used an interval over a timeout here to ensure it only occurs once in my test.
export class MyCoolClass {
private source = new BehaviorSubject(false);
source$ = this.source.asObservable();
constructor() {
this.source$.subscribe((value) => {
console.log('my source', value);
});
window.setInterval(() => {
this.source.next(true);
}, 2000);
}
}
What is a clean way to handle this? A long time ago I wrote some annoying subscription:
export class MyCoolClass {
private source = new BehaviorSubject(false);
source$ = this.source.asObservable();
private mySubscription?: Subscription;
constructor() {
this.mySubscription = this.source$.subscribe((value) => {
if(value === true){
this.mySubscription?.unsubscribe();
console.log('my source', value);
}
});
window.setInterval(() => {
this.source.next(true);
}, 2000);
}
}
I feel as though I always end up with a subscription that I need to deal with.
Is there a cleaner way to achieve this?
Thanks 😊
EDIT
I believe this works, but it still seems a little bloated?
export class MyCoolClass {
private source = new BehaviorSubject(false);
source$ = this.source.asObservable();
constructor() {
this.source$
.pipe(
skipWhile(value => value === false),
first()
)
.subscribe((value) => {
console.log('my source', value);
});
window.setInterval(() => {
this.source.next(true);
}, 2000);
}
}

One way to simplify this would be :
export class MyCoolClass {
private source = new Subject();
source$ = this.source.asObservable();
constructor() {
this.source$
.pipe(
first()
)
.subscribe((value) => {
console.log('my source', value);
});
window.setInterval(() => {
this.source.next(true);
}, 2000);
}
}
Notably, making the BehaviourSubject into a Subject. BehaviourSubject specifically is for providing a default value to any subscribers. But in your case, you don't want a default value, infact you're skipping it entirely. So at this point, it would be simpler to simply move to a Subject and remove the skipping.
For a little more info on Subject vs BehaviorSubject (And ReplaySubject) here's a quick article : https://tutorialsforangular.com/2020/12/12/subject-vs-replaysubject-vs-behaviorsubject/

Related

Passing value from one RxJS operator to another

Here is my code:
#Injectable()
export class TraitementDetailEffects {
ingoing_loadDetail: { traitementID: number, obs: Promise<any> };
#Effect()
loadTraitementDetail$: Observable<Action> = this.actions$.pipe(
ofType(ETraitementDetailActions.loadTraitementDetail),
map((action: LoadTraitementDetail) => action.payload),
switchMap((traitementID) => {
if (this.ingoing_loadDetail && this.ingoing_loadDetail.traitementID === traitementID) {
return this.ingoing_loadDetail.obs;
}
const obs = this.traitementsService.loadDetail(traitementID);
this.ingoing_loadDetail = {traitementID: traitementID, obs: obs};
return obs;
}),
map(result => {
this.ingoing_loadDetail = null;
//here I don't have access to traitementID :'(
return new LoadTraitementDetailSuccess(traitementID, result);
})
);
constructor(
private actions$: Actions,
private traitementsService: TraitementsService
) {
}
}
I'm trying to pass the variable or value traitementID to the last map.
I tried to avoid the last map with an async await but then I get a weird errors "Effect dispatched an invalid action" and "Actions must have a type property" (FYI all my actions have a type property).
Try to bake this id into observable's resolve, like:
switchMap((traitementID) => {
return this.traitementsService.loadDetail(traitementID).pipe(
map(detail => ({detail,traitementID}))
);
}),
map(({detail,traitementID}) => {
...
})

How to make the observable still fire without subscribing?

I have the following code =>
https://stackblitz.com/edit/angular-3avdpv
this.joystickStart$ = Observable.create(observer => {
this.joystickManager.on('start', (evt, nipple) => {
console.log("START")
observer.next(nipple);
});
});
this.joystickMove$ = Observable.create(observer => {
this.joystickManager.on('move', (evt, nipple) => {
console.log("MOVE")
this.lastEvent = nipple;
clearInterval(this.lastInterval);
this.lastInterval = setInterval(() => {
observer.next(this.lastEvent);
}, this.refireFrequency);
observer.next(nipple);
});
});
this.joystickRelease$ = Observable.create(observer => {
this.joystickManager.on('end', (evt, nipple) => {
console.log("END")
clearInterval(this.lastInterval);
observer.next(nipple);
});
});
the problem I face is, if someone subscribe to the joystickStart$, joystickMove$, joystickEnd$ the content of the observable is fired. but if none do this (or subscribe only to for exemple movement, the start and end are not fired.
but, this break my system, cause the setInterval will not be cleared.
how to make it work even without subscriber ? should I autosubscribe ?
As per your question, it appears you want to do following -
As soon as joystick starts, track the joystick move and do that until the first joystick release comes. If my understanding is correct then instead of using setInterval or [imperative approach], you can use rxjs operators [reactive approach] and various Subjects like this:
export class JoystickComponent implements AfterViewInit {
#ViewChild('joystick') joystick: ElementRef;
#Input() options: nipplejs.JoystickManagerOptions;
private lastEvent: nipplejs.JoystickOutputData;
private refireFrequency: 1000;
private lastInterval: any;
private joystickManager: nipplejs.JoystickManager;
joystickStart$ = new Subject<nipplejs.JoystickOutputData>();
joystickMove$ = new Subject<nipplejs.JoystickOutputData>();
joystickRelease$: Subject<nipplejs.JoystickOutputData> = new Subject<nipplejs.JoystickOutputData>();
constructor() { }
ngAfterViewInit() {
this.create();
}
create() {
this.joystickManager = nipplejs.create({
zone : this.joystick.nativeElement,
position: {left: '50%', top: '50%'},
mode: 'semi'
});
this.joystickManager.on('start', (evt, nipple) => {
this.joystickStart$.next(nipple);
});
this.joystickManager.on('move', (evt, nipple) => {
this.joystickMove$.next(nipple);
});
this.joystickManager.on('end', (evt, nipple) => {
this.joystickRelease$.next(nipple);
});
//combine start and move events
//and do that until you hit first released event
combineLatest(this.joystickStart$
.pipe(tap(_ => console.log(`Joystick Started`))),
this.joystickMove$
.pipe(tap(_ => console.log(`Joystick Moved`)))
)
.pipe(
takeUntil(this.joystickRelease$.pipe(tap(_ => console.log(`Joystick released`)))),
//If you want to repeat the observable then use repeat operator
//repeat()
).subscribe(([start, move]) => {
console.log({start}, {move});
}, () => {}, () => console.log('complete'));
}
}
Working stackblitz - https://stackblitz.com/edit/angular-fazjcf?file=src/app/joystick/joystick.component.ts
Change to using subjects and have the logic in a subscription to the subject.
this.joystickRelease$ = new Subject();
this.joystickRelease$.subscribe(
nipple => { clearInterval(this.lastInterval); }
);
this.joystickManager.on('end', (evt, nipple) => {
this.joystickRelease$.next(nipple);
});

Get only distinct values from an ngrx store selector

I have a function which checks whether a grid has loaded or not and if not it triggers the loading. But currently this ends up firing several times for the same Loaded value so it will call the relevant action multiple times. I was under the impression that store selectors emit by default only distinct (changed) values?
My function
private gridLoaded(filters: FilteredListingInput): Observable<boolean> {
return this.settings.states.Loaded.pipe(
tap(loaded => {
this.logService.debug(
`Grid ID=<${
this.settings.id
}> this.settings.states.Loaded state = ${loaded}`
);
// Now we get duplicate firings of this action.
if (!loaded) {
this.logService.debug(
`Grid Id=<${
this.settings.id
}> Dispatching action this.settings.stateActions.Read`
);
this.store.dispatch(
new this.settings.stateActions.Read(filters)
);
}
}),
filter(loaded => loaded),
take(1)
);
}
this.settings.states.Loaded is selector from NgRx store.
The logging output I get looks like this:
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = false {ignoreIntercept: true}
Grid Id=<grid-reviewItem> Dispatching action this.settings.stateActions.Read {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
How can I make sure that the relevant actions are triggered only once?
Edit - updates
Selector code:
export const getReviewItemsLoaded = createSelector(
getReviewItemState,
fromReviewItems.getReviewItemsLoaded
);
export const getReviewItemState = createSelector(
fromFeature.getForecastState,
(state: fromFeature.ForecastState) => {
return state.reviewItems;
}
);
export const getReviewItemsLoaded = (state: GridNgrxState<ReviewItemListDto>) =>
state.loaded;
export interface GridNgrxState<TItemListDto> {
allItems: TItemListDto[];
filteredItems: TItemListDto[];
totalCount: number;
filters: FilteredListingInput;
loaded: boolean;
loading: boolean;
selectedItems: TItemListDto[];
}
As you can see we are just getting the state.loaded property, it's a trivial selector.
Reducers that change the loading property:
export function loadItemsSuccessReducer(state: any, action: GridAction) {
const data = action.payload;
return {
...state,
loading: false,
loaded: true,
totalCount: data.totalCount ? data.totalCount : data.items.length,
allItems: data.items
};
}
export function loadItemsReducer(state: any, action: GridAction) {
return {
...state,
loading: true,
filters: action.payload
};
}
export function loadItemsFailReducer(state: any, action: GridAction) {
return {
...state,
loading: false,
loaded: false
};
}
Actions
export class LoadReviewItemsAction implements Action {
readonly type = LOAD_REVIEWITEMS;
constructor(public payload?: FilteredListingInput) {}
}
export class LoadReviewItemsFailAction implements Action {
readonly type = LOAD_REVIEWITEMS_FAIL;
constructor(public payload: any) {}
}
export class LoadReviewItemsSuccessAction implements Action {
readonly type = LOAD_REVIEWITEMS_SUCCESS;
constructor(public payload: PagedResultDtoOfReviewItemListDto) {}
Effects
export class ReviewItemsEffects {
constructor(
private actions$: Actions,
private reviewItemApi: ReviewItemApi
) {}
#Effect()
loadReviewItems$ = this.actions$
.ofType(reviewItemActions.LOAD_REVIEWITEMS)
.pipe(
switchMap((action: reviewItemActions.LoadReviewItemsAction) => {
return this.getDataFromApi(action.payload);
})
);
/**
* Retrieves and filters data from API
*/
private getDataFromApi(filters: FilteredListingInput) {
return this.reviewItemApi.getReviewItems(filters || {}).pipe(
map(
reviewItems =>
new reviewItemActions.LoadReviewItemsSuccessAction(
reviewItems
)
),
catchError(error =>
of(new reviewItemActions.LoadReviewItemsFailAction(error))
)
);
}
}
I was able to work around the issue by refactoring the gridLoaded method into waitForGridLoaded and moving some of its logic outside of it. This works well but I couldn't solve the original issue of why the tap(loaded => ...) logic is triggered many times.
Now the relevant bits look like this (it doesn't feel like the nicest solution):
private initializeLoadingState() {
const loadingStateSubscription = this.settings.states.Loading.subscribe(
loading => {
this.loading = loading;
}
);
this.addSubscription(loadingStateSubscription);
}
private initializeLoadedState() {
const loadedStateSubscription = this.settings.states.Loaded.subscribe(
loaded => {
this.loaded = loaded;
}
);
this.addSubscription(loadedStateSubscription);
}
onLazyLoad(event: LazyLoadEvent) {
// Do nothing yet if we are expecting to set parent filters
// but we have not provided any parent filter yet
if (
this.settings.features.ParentFilters &&
(!this.parentFiltersOnClient ||
!this.parentFiltersOnClient.length) &&
(!this.parentFiltersOnServer || !this.parentFiltersOnServer.length)
) {
return;
}
this.loadAndFilterItems(event);
}
private loadAndFilterItems(event: LazyLoadEvent) {
if (this.settings.features.ClientSideCaching) {
if (this.loaded) {
// Load only once and filter client side
this.store.dispatch(
new this.settings.stateActions.FilterClientSide(
this.buildFilters(event, GridParentFilterTypes.Client)
)
);
} else if (!this.loading) {
// Start loading in from server side
this.store.dispatch(
new this.settings.stateActions.Read(
this.buildFilters(event, GridParentFilterTypes.Server)
)
);
// When we have finished loading, apply any client side filters
const gridLoadedSubscription = this.waitForGridLoaded().subscribe(
loaded => {
if (loaded) {
this.store.dispatch(
new this.settings.stateActions.FilterClientSide(
this.buildFilters(
event,
GridParentFilterTypes.Client
)
)
);
}
}
);
this.addSubscription(gridLoadedSubscription);
}
} else {
this.store.dispatch(
new this.settings.stateActions.Read(
this.buildFilters(event, GridParentFilterTypes.Server)
)
);
}
}
private waitForGridLoaded(): Observable<boolean> {
return this.settings.states.Loaded.pipe(
filter(loaded => loaded),
take(1)
);
}

RxJS multiple switchMap and map operators... is there a better way?

I'm pretty new to RxJS and am wondering if I am doing this right... in the ngOnInit() function below, I get a client object, then pipe it...
Is there a better way to do the repeat switchMap/map operations below?
My code works... but I am wondering if there is a more elegant approach that I should be adopting...
public client: Client;
public contract: Contract;
public alreadyPendingContract: boolean;
public alreadyActiveContract: boolean;
public minimumStartDate: Date;
public minimumEndDate: Date;
public rolloverExclusionDate: Date;
public startDateFilter;
ngOnInit() {
this.clientService.getClient$().pipe(
filter(client => client != null),
map(client => this.client = client),
pluck('client_id'),
map((client_id: string) => {
this.clientContractForm.get('client_id').setValue(client_id);
return client_id;
}),
switchMap((client_id: string) => {
return this.contractAddService.getAlreadyPendingContract$(client_id);
}),
map(alreadyPendingContract => {
this.alreadyPendingContract = alreadyPendingContract;
return this.client.client_id;
}),
switchMap((client_id: string) => {
return this.contractAddService.getAlreadyActiveContract$(client_id);
}),
map(alreadyActiveContract => {
this.alreadyActiveContract = alreadyActiveContract;
}),
switchMap(() => {
return this.contractAddService.getMinimumStartDate$(this.client.client_id);
}),
map((minimumStartDate: IMinimumStartDate) => {
this.minimumStartDate = minimumStartDate.minimumStartDate;
this.rolloverExclusionDate = minimumStartDate.rolloverExclusionDate;
this.startDateFilter = (m: Moment): boolean => {
// Filters out the rollover exclusion day from being an available start date.
return !moment.utc(m).isSame(moment.utc(this.rolloverExclusionDate), 'day');
}
})
).subscribe();
}
I am not sure this is more elegant, but it is an alternative way
ngOnInit() {
this.clientService.getClient$().pipe(
filter(client => client != null),
map(client => {
this.client = client;
this.clientContractForm.get('client_id').setValue(client_id);
return client.client_id;
},
switchMap(client_id => this.doStuffWithClientId(client_id)),
map((minimumStartDate: IMinimumStartDate) => {
this.minimumStartDate = minimumStartDate.minimumStartDate;
this.rolloverExclusionDate = minimumStartDate.rolloverExclusionDate;
this.startDateFilter = (m: Moment): boolean => {
// Filters out the rollover exclusion day from being an available start date.
return !moment.utc(m).isSame(moment.utc(this.rolloverExclusionDate), 'day');
}
})
).subscribe();
}
doStuffWithClientId(clientId: string) {
return this.contractAddService.getAlreadyPendingContract$(client_id).pipe(
tap(alreadyPendingContract => this.alreadyPendingContract = alreadyPendingContract),
switchMap(() => this.contractAddService.getAlreadyActiveContract$(clientId)),
tap(alreadyActiveContract => this.alreadyActiveContract = alreadyActiveContract),
switchMap(() => this.contractAddService.getMinimumStartDate$(clientId)),
)
}
I have not tested the code, so there may well be syntax mistakes. The basic idea though is to isolate all the things which depend on client_id into one function which receives the clientId as input and therefore makes it visible throughout the entire function.

How to get an observable to return data immediately and every 5 seconds thereafter

I want to create an observable that returns data from a webapi. I'd like it to return the data immediately, and poll the API every 10 seconds. The code below shows I'm using the 'interval' method. But this delays the first set of data by 10 seconds. How do I get that first flush of data to come down with no initial delay?
export class EventService {
public events$: Observable<Event[]>;
private _eventsObserver: Observer<Event[]>;
private pollInterval: number = 5000;
private _dataStore: {
events: Event[];
};
constructor(private http: Http) {
this._dataStore = { events: [] };
this.events$ = new Observable(observer => this._eventsObserver = observer)
.startWith(this._dataStore.events)
.share();
}
pollEvents() {
return Observable.interval(10000)
.switchMap(() => {
return this.http.get('app/resources/data/scheduleevents.json')
.map((responseData) => {
return responseData.json();
});
})
.map((events: Array<any>) => {
let result: Array<Event> = [];
if (events["data"]) {
events["data"].forEach((event) => {
result.push(event);
});
}
return result;
});
}
}
Got it:
.interval(5000)
.startWith(0);
Use timer. I think the timer is what you need (see RxJS tab):
http://reactivex.io/documentation/operators/timer.html#collapseRxJS
Could be used like:
Observable.timer(0, 5000).flatMap(() => apiCall())
Where 0 - delay before emitting the first value, 5000 - emit value after each 5s
let timer = TimerObservable.create(0, 5000);
this.sub = timer.subscribe(t => {
this.yourMethod()
});
To unsubscribe run this.sub.unsubscribe()
I personnally use interval with startWith (need RxJs 6+), here is a complete example:
history: any;
historySubscription: Subscription;
constructor(private jobService: JobService) { }
ngOnInit() {
this.historySubscription = interval(10000).pipe(
startWith(0),
flatMap(() => this.jobService.getHistory())
).subscribe(data => {
this.history = data;
});
}
ngOnDestroy() {
this.historySubscription.unsubscribe();
}
This retrieves history on init and then every 10 seconds.
Another alternative is using timer as explained by #Alendorff.
For angualr2 below is the code i have written in my application and it is working as expected -
In service --
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';
getList(): Observable<IVM> {
return Observable.interval(5000).startWith(0)
.switchMap(() =>
this._http.get(this._vmURL )
.map((response: Response) => <IVM>response.json().data)
.do(data => console.log('All: ' + JSON.stringify(data)))
.catch(this.handleError)
);
}
In component --
private getInstanceDetails(): void {
this._vmDataService.getList()
.subscribe(vmList => {
//Do whatever you want with the vmList here :)
},
error => this.errorMessage = <any>error);
}
Thanks,
Kindly let me know your thoughts.
Observable.interval(5L, TimeUnit.SECONDS)
.startWith(0)
.observeOn(AndroidSchedulers.mainThread())
.map { foobar() }
works fine for me.
Thanks

Resources