I have been trying to observe changes on a Map Object in JavaScript but for some reason only can observe the creation of the object. Do observables not work when adding/removing data from a Map?
Here is the observable:
test(): Observable<Map<string, Object>> {
return of(this.testModel.test());
}
This is me subscribing to it:
test(): Observable<Map<string, Object>> {
let mapOb = this.testModel.test();
return Observable.create((obsrvr) => {
const originalSet = mapOb.set;
const originalDelete = mapOb.delete;
mapOb.set = (...args) => {
obsrvr.next(originalSet.call(mapOb, ...args));
};
mapOb.delete = (...args) => {
obsrvr.next(originalDelete.call(mapOb, ...args));
}
});
}
I see the log statement during the creation of the Map, but if i add any new entries to the Map nothing is logged. Anyone know why this may be happening?
I get an error at maoOb.set and mapOb.delete:
Type '(key: string, value: Object) => void' is not assignable to type '(key: string, value: Object) => Map<string, Object>'.
Type 'void' is not assignable to type 'Map<string, Object>'
You current approach doesn't seem correct when you want to listen to addition/deletion of data in a Map.
You are simply returning an Observable using of(Object Reference), this will no way know about things that you are doing with the Object you are passing with it.
You need to have an Observable which emits when you perform set() or delete() over the MapInstance.
You may modify your Map instance this way to achieve what you desire.
createObservable(mapOb) {
return Observable.create((obsrvr) => {
const originalSet = mapOb.set;
const originalDelete = mapOb.delete;
mapOb.set = (...args) => {
const setReturn = originalSet.call(mapOb, ...args);
obsrvr.next(setReturn);
return setReturn;
};
mapOb.delete = (...args) => {
const deleteReturn = originalDelete.call(mapOb, ...args);
obsrvr.next(deleteReturn);
return deleteReturn;
}
});
}
Pass the map to createObservable() method and subscribe to it. In this method, I have modified the set and delete methods of your map, so that it emits a value when those methods are called.
I have created a dummy example for the answer: Link.
Related
I want to return a ClientDetails object with a loaded image.
So retrieve an Observable, and modify the value with another Observable and return the whole Observable.
I hope the code below indicates what I am trying to do, but I know it can be done much cleaner using RxJS operators. Anyone know how to?
interface ClientDetails {
team: Member[];
}
interface Member {
id: number;
image: string;
}
this.clientDetails$ = this.clientService.getClientDetails().subscribe((details: ClientDetails) => {
details.team.forEach(member => {
this.imageService.getImage(member.id).subscribe((image: string) => {
member.image = image
}
}
}
You're right in assuming RxJS operators would make it more elegant. At the moment the variable this.clientDetails$ doesn't hold an observable and it wouldn't work as you'd expect it to.
Instead you could use higher order mapping operator switchMap to switch from one observable to another (it's better to avoid nested subscriptions in general) and forkJoin function to trigger multiple observables in parallel. You could also use JS destructing and RxJS map operator to return the object with all it's contents.
Try the following
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) =>
forkJoin(
details.team.map(member =>
this.imageService.getImage(member.id).pipe(
map(image: string => ({...member, member.image: image}))
)
)
).pipe(
map((team: any) => ({...details, details.team: team}))
)
);
);
Note: I didn't test the code. Please check if the object returned is what you actually require.
Try
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) => {
const images$: Observable<string[]> = forkJoin(
details.team.map(member => this.imageService.getImage(member.id))
);
return forkJoin([of(details), images$]);
}),
map(([details, images]) => {
return {
team: _.zipWith(details.team, images, (d, m) => d.image = m) // zipWith = lodash function
};
}),
).subscribe((details: ClientDetails) => console.log(details));
Lets say i have an observable that emits employees.
$employees
I wish to manipulate a sub property of each employee based on an Observable. For the example lets say display name.
Currently im performing the task like this.
const getDisplayName = (emp) => {}; //returns an observable
const mapFn = async (emp) => {
emp.displayName = await getDisplayName(emp).toPromise();
return emp;
}
$employees
.pipe(mergeMap(mapFn));
I think my confusion is that, to my understanding, we have two streams. The root $employees and the getDisplayName. My understanding is with the various merge operators the root value would be replaced by the value of the secondary stream. Not merged with.
Is there a better way to do this where i don't need to convert to a promise but can also just map a property of the employee?
Thanks.
you want to do:
$employees
.pipe(mergeMap(employees => {
return forkJoin(employees.map(emp => getDisplayName(emp).pipe(
map(displayName => ({...emp, ...{displayName}}))
)))
}));
if you really want a broken out mapFn:
const mapFn = employees => {
return forkJoin(employees.map(emp => getDisplayName(emp).pipe(
map(displayName => ({...emp, ...{displayName}}))
)));
}
$employees
.pipe(mergeMap(mapFn))
always advise against mixing rxjs and async / await. They're different methods of handling async operations and don't play nice. mergeMap needs you to return an observable, forkJoin executes observables in parralel, so you join all your employees mapped into their getName functions and then map the name into the original employee and return it.
EDIT: the above is if $eployees is emitting an array of employees. if it's just a single employee, do:
const mapFn = emp => {
return getDisplayName(emp).pipe(
map(displayName => ({...e, ...{displayName}}))
);
}
however, if it's emitting a single employee (or an array of employees) multiple times, it's important to understand the implications of using mergeMap vs switchMap vs concatMap. Let me know if that is the case.
This should work:
const emp$ = from([1, 2, 3]);
function getName(emp: number): Observable<string> {
return of(emp.toString());
}
const empNameTuple$ = emp$.pipe(mergeMap(emp => {
return getName(emp).pipe(map(name => [emp, name]));
}))
Here I'm just returning a tuple of [number, string], but you can map it however you wish.
I have the following function definition:
const useScroll = () => {
const ref = useRef(null)
function executeScroll() {
if (ref !== null)
window.scrollTo(0, ref.current.offsetTop)
}
const htmlElementAttributes = { ref }
return [executeScroll, htmlElementAttributes]
}
export default useScroll;
Based on this function, I have the following code:
const [executeScroll, scrollHtmlAttributes] = useScroll();
const click_argueBtn = (e) => {
e.preventDefault();
executeScroll();//error
}
However, the executeScroll(); code throws the following error:
Error: Cannot invoke an expression whose type lacks a call signature
Any ideas why I receive this error? My code is based on this post.
Typescript is doing its best to determine the types automatically, and it determines that useScroll returns an array, who's elements are each () => void | { ref: /*ref type*/ } (i don't know precisely what the type on the ref object is). When you try to call executeScroll, it doesn't know whether it's a function, or an object with a ref. So since it might not be a function, you're not allowed to call it.
Instead, i'd recommend explicitly telling typescript that useScroll returns a tuple. A tuple is similar to an array, except you explicitly declare how many elements it has and what their individual types are:
const useScroll = (): [() => void, { ref: /* ref type */ }] => {
// ...etc
}
or
const useScroll = () => {
// ... etc
return [executeScroll, htmlElementAttributes] as [() => void, { ref: /* ref type */ }];
}
or if you don't like it inline, you could extract it to a type:
export type UseScrollResult = [() => void, { ref: /* ref type */ }];
const useScroll = (): UseScrollResult => {
// ...etc
}
From this, typescript now knows that element 0 of the array is () => void, and therefore it's legal to call it as a function.
I have these two methods which are almost similar:
private firstFunction () {
this.serviceOne.methodOne().subscribe(
res => {
return resultOne = res;
},
err => {}
);
}
private secondFunction () {
this.serviceTwo.methodTwo().subscribe(
res => {
return resultTwo = res;
},
err => {}
);
}
I want to write a generic function, like this:
genericFunction (service ,method , result ) {
service.method().subscribe(
res => {
return result = res;
},
err => {}
);
}
And consequently I want to get something like this working:
genericFunction (serviceOne , methodOne , resultOne );
genericFunction (serviceTwo , methodTwo , resultTwo );
Actually, I cannot find how to pass methodOne and methodTwo as params. Any sugestions?
There are several issues in your code.
Firstly, you want to modify the field you pass in as a parameter (as suggested by result = res. You can't pass in a reference to a field, but you can pass in the field name, and use indexing to change the field. keyof T will allow you to pass in the field in a type safe way.
Secondly if you want to access a method on a service. Again we can do this passing in the method name, and we can constrain the service to have a method with the passed in method name, that returns an Observable. The result of the Observable can also be constrained to be of the same type of the field we are going to assign it to in order for the method to be fully type safe.
declare class Service1 {
method1() : Observable<number>
}
declare class Service2 {
method2() : Observable<string>
}
class MyClass {
resultOne!: number;
resultTwo!: string;
constructor() {
this.genericFunction(new Service1(), "method1", "resultOne");
this.genericFunction(new Service2(), "method2", "resultTwo");
this.genericFunction(new Service1(), "method1", "resultTwo"); // error resultTwo is a string, the method return Observable<number>
this.genericFunction(new Service2(), "method", "resultTwo"); // error method does not exit on Service2
this.genericFunction(new Service2(), "method2", "resultTwo2"); // error field does not exist on type
}
genericFunction<MethodKey extends string, ResultKey extends keyof MyClass>(service:Record<MethodKey, ()=> Observable<MyClass[ResultKey]>>, method:MethodKey, result: ResultKey){
service[method]().subscribe(
res => this[result] = res,
err => {}
);
}
}
Note We could have also passed in the function as a function not just as a name, but directly a typed function. The disadvantage of this is that we either have to use bind to ensure the service method will still have the correct this when it's called, or use an arrow function when calling (again to ensure the service method has the correct this). This is error prone though, bind results in an untyped function, so we can't check compatibility to the field, and someone might pass service.method directly and no error would be reported until runtime:
class MyClass {
resultOne!: number;
resultTwo!: string;
constructor() {
var service1 = new Service1()
var service2 = new Service2()
this.genericFunction(()=> service1.method1(), "resultOne");
this.genericFunction(()=> service2.method2(), "resultTwo");
this.genericFunction(service2.method2, "resultTwo"); // no error, depending on the implementation of method2 it might or might not work
this.genericFunction(service2.method2.bind(service2), "resultOne"); // no error, the service call will work, but we store it in an incompatible variable
this.genericFunction(()=> service1.method1(), "resultTwo");// error resultTwo is a string, the method return Observable<number>
this.genericFunction(()=> service2.method2(), "resultTwo2");// // error field does not exist on type
}
genericFunction<MethodKey extends string, ResultKey extends keyof MyClass>(method:()=> Observable<MyClass[ResultKey]>, result: ResultKey){
method().subscribe(
res => this[result] = res,
err => {}
);
}
}
try by using the following code:
private firstFunction () {
let response= genericFunction(this.serviceOne.methodOne())
}
private secondFunction () {
let response = genericFunction(this.serviceTwo.methodTwo())
}
Modify you Generic Function by just receiving a variable.
//if it is angular 4 or less
genericFunction (method: Observable) {
return method.map(res => {
return res.json();
});
}
//if it is angular 5 or 6
genericFunction (method: Observable) {
return method.pipe(
map(res => {
return res;
}));
}
I try to adjust a working example from ngx-chips to my needs. This is how the onRemoving method example looks like:
public onRemoving(tag: TagModel): Observable<TagModel> {
const confirm = window.confirm('Do you really want to remove this tag?');
return Observable
.of(tag)
.filter(() => confirm);
}
Now instead of windows.confirm I want to use a custom component that has a AskQuestion method with the following signatur:
AskQuestion(question: string, yesCallback: () => void, noCallback?: () => void): void {
So now I have multiple callbacks but the ngx-chips components expect that I return an observable. I tried to convert the callback to an observable using the bindCallback method:
public onRemoving(tag: TagModel): Observable<TagModel> {
const choiceCallback = (choice: boolean): TagModel=> {
if (choice)
return tag;
};
this.questionService.AskQuestion("Remove item?", () => choiceCallback(true), () => choiceCallback(false))
return Observable.bindCallback(choiceCallback);
}
But it looks like I am doing it wrong. Any ideas?
The definition of bindCallback() reads:
Give it a function f of type f(x, callback) and it will return a function g that when called as g(x) will output an Observable.
And your usage does not fit this description. choiceCallback() does not return a function that returns an observable.
Use an Observable constructor instead:
public onRemoving(tag: TagModel): Observable <TagModel> {
return Observable.create(observer => {
const choiceCallback = (choice: boolean) => {
if (choice) {
observer.next(tag);
}
observer.complete();
};
this.questionService.AskQuestion("Remove item?", () => choiceCallback(true), () => choiceCallback(false));
});
}
I'm not 100% familiar with this stack, but as far as I could see, looks like bindCallback returns a function which returns an Observable (docs).
So maybe you need to call it to get an observable and use it on your return statement?
Since in your function signature says it would return an Observable type.
You could try to replace the return statement by:
return Observable.bindCallback(choiceCallback)()