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);
});
Related
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/
I have an NGRX effect that - depending on the state - emits an action with a delay or it emits nothing.
I want to write a test, covering both situations.
This is the effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
filter(state => state.foo === false),
delay(4000),
map(state => myOtherAction())
)
);
The test for the situation where it should emit the otherAction with the delay works fine:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});
But I have no clue how to test the other state, where it should NOT emit another action (the stream has zero length):
it('should return an empty stream', () => {
store.setState({
myFeature: {
foo: true
}
});
// ???
});
Please help :)
This will be tough to do because the filter will prevent the effect to ever return an observable.
Option 1:
// wherever you're dispatching MyAction, only dispatch it if the foo property is true on the created action
Option 2:
// Change the structure of the effect to return empty
import { EMPTY } from 'rxjs';
....
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
delay(4000),
map(state => state.foo ? myOtherAction() : EMPTY)
)
);
The test:
import { EMPTY } from 'rxjs';
....
describe('emit my action', () => {
const action = MyAction();
action.foo = false; // set foo property to false
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: EMPTY // assert now it is empty
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
})
Due to the hint from AliF50, I replaced the filter in the chain (which stops the Observable from emitting), by a "Noop Action" (= a normal action without any listeners on it). So instead of checking the foo property in the filter, I return the noopAction in the map, when foo is true, and the otherAction when it's false.
The effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
//filter(state => state.foo === false),
delay(4000),
map(state => state.foo !== false ? noopAction() : myOtherAction())
)
);
The test:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
it('should return a stream with noop action as foo is true', () => {
store.setState({
myFeature: {
foo: true
}
});
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: NoopAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});
We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}
During dispatch, my effect is called repeatedly until my backend responds and the data is loaded. I need help in understanding how to load the data with just one GET REQUEST and then load from the store if the data is actually already present.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
filter((cases) => {
return cases.length > 0;
}),
tap(() => {
this.store.dispatch(new ImportTaskLoadCasesLoadedFromStoreAction());
}),
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
map((action: ImportTaskLoadCasesAction) => action),
switchMap((payload) => {
return this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({ total: response['count'], cases: response['results'] });
}),
catchError((error) => {
this.logger.error(error);
return of(new ImportTaskLoadCasesLoadErrorAction(error));
})
);
})
);
Yes i have a reducer for handeling my Success Action like this :
case ImportCasesActionTypes.ImportTaskLoadCasesSuccessAction:
return {
...state,
loading: false,
cases: action.payload.cases,
total: action.payload.total
};
It's called in my effects.
Does the below work? This is assuming you have a reducer that handles the ImportTaskLoadCasesSuccessAction; Maybe supplying a working example will help, as there is a bit of guessing as how state is being managed.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
// personally, I would have the component/obj that is consuming this.cases$ null check the cases$, removed for brevity
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
mergeMap(() => this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({
total: response['count'],
cases: response['results']
});
}),
// catch error code removed for brevity
);
)
);
If you only want the call this.importCases.get() to fire one time, I suggest moving the action dispatch out of the .pipe(tap(...)). As this will fire every time a subscription happens.
Instead, set up this.cases$ to always return the result of select(selectImportTaskCasesData),. Functionally, you probably want it to always return an array. But that is up to your designed desire.
Foe example ...
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
);
Separately, like in a constructor, you can dispatch the this.store.dispatch(new ImportTaskLoadCasesAction());. If you want it to only get called when cases$ is empty, you can always wrap it in a method.
e.g.
export class exampleService() {
ensureCases(): void {
this.store.pipe(
select(selectImportTaskCasesData),
take(1)
).subscribe(_cases => {
if (_cases && _cases.length < 1 ) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
}
}
I am learning to use RXJS. In this scenario, I am chaining a few async requests using rxjs. At the last mergeMap, I'd like to have access to the first mergeMap's params. I have explored the option using Global or withLatest, but neither options seem to be the right fit here.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => {
return readCSVFile(gauge.id);
}),
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id))),
catchError(error => of(`Bad Promise: ${error}`))
);
readCSVFile is an async request which returns an observable to read CSV from a remote server.
readStringToArray is another async request which returns an observable to convert string to Arrays
transposeArray just does the transpose
uploadToDB is async DB request, which needs gague.id from the first mergeMap.
How do I get that? It would be great to take some advice on why the way I am doing it is bad.
For now, I am just passing the ID layer by layer, but it doesn't feel to be correct.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id)),
mergeMap(({ data, gaugeId }: any) => readStringToArray(data, gaugeId)),
map(({ data, gaugeId }) => transposeArray(data, gaugeId)),
mergeMap(({ data, gaugeId }) => uploadToDB(data, gaugeId)),
catchError(error => of(`Bad Promise: ${error}`))
);
Why don't you do simply this?
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id)))
)),
catchError(error => of(`Bad Promise: ${error}`))
);
You can also wrap the inner observable in a function:
uploadCSVFilesFromGaugeID(gaugeID): Observable<void> {
return readCSVFile(gaugeID).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gaugeID))
);
}
In order to do this at the end:
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => uploadCSVFileFromGaugeID(gauge.id)),
catchError(error => of(`Bad Promise: ${error}`))
);
MergeMap requires all observable inputs; else, previous values may be returned.
It is a difficult job to concatenate and display the merging response. But here is a straightforward example I made so you can have a better idea. How do we easily perform sophisticated merging.
async playWithBbservable() {
const observable1 = new Observable((subscriber) => {
subscriber.next(this.test1());
});
const observable2 = new Observable((subscriber) => {
subscriber.next(this.test2());
});
const observable3 = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next(this.test3());
subscriber.complete();
}, 1000);
});
console.log('just before subscribe');
let result = observable1.pipe(
mergeMap((val: any) => {
return observable2.pipe(
mergeMap((val2: any) => {
return observable3.pipe(
map((val3: any) => {
console.log(`${val} ${val2} ${val3}`);
})
);
})
);
})
);
result.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
}
test1() {
return 'ABC';
}
test2() {
return 'PQR';
}
test3() {
return 'ZYX';
}