I want to create a new operator like map. but the different is whatever map returns I need to clone it using JSON.parse(JSON.stringify(value)).
Because I return an object from my store (Subject), and I don't want somebody to change it, or it's inner properties.
I copy the map singture to my mapToVM function, but I can't find any way to use pipe or manipulate the return results.
Is it possible to change the return results by wrapping the map operator?
codesandbox
So I try to return the map function and take the source as Observable, but typescript
import { map, Observable, OperatorFunction, Subject } from "rxjs";
console.clear();
const store = new Subject<{ company: { id: number } }>();
store.pipe(map((s) => s.company)).subscribe((company) => {
console.log({ company });
});
function mapToVM<T, R>(
project: (value: T, index: number) => R
): OperatorFunction<T, R> {
// JSON.parse(JSON.stringify(source));
return map(project);
}
store.pipe(mapToVM((s) => s.company)).subscribe((company) => {
console.log({ company });
});
store.next({ company: { id: 1 } });
you can use the static pipe function:
function jsonClone<T>(): OperatorFunction<T, T> {
return pipe(map((source) => JSON.parse(JSON.stringify(source))));
}
Full CodeSandbox example
Related
I have this observable object in my angular project that has this type:
export interface FavoritesResponse {
wallet: boolean;
deposit: boolean;
withdraw: boolean;
transfer: boolean;
exchange: boolean;
ticket: boolean;
account: boolean;
}
I want to extract an array from this object with only the properties that have the value true.
So for example if my favorites object looks like this:
favorites$ = {
wallet: true;
deposit: true;
withdraw: false;
transfer: false;
exchange: false;
ticket: true;
account: true;
}
I want to have my enabledFavorites$ look like this:
enabledFavorites$ = [
wallet,
deposit,
ticket,
account
]
as in, turn it into an array and only have the keys that had the value of true. How can I do this? I know the solution probably contains an rxjs pipe, map but I don't know what I should be doing exactly.
If you mean to say the observable emits an object of type FavoritesResponse and you wish to transform the emission to an array of it's keys only with value true, you could use
RxJS map operator to transform the incoming object
Native JS methods Object.keys() with Array#filter to perform the actual transformation
enabledFavorites$: Observable<string[]> = favorites$.pipe(
map((favs: FavoritesResponse) =>
Object.keys(favs).filter((key: string) => !!favs[key])
)
);
get favoritesArray$(): Observable<string[]> {
return this.favoritesSettings$.pipe(
map((favorites) => {
const _items = Object.keys(favorites);
const _result: string[] = [];
_items.forEach((item) => {
if (!!(favorites as any)[item]) {
_result.push(item);
}
});
return _result;
})
);
}
So I guess that you want is convert from Observable<FavoritesResponse> to Observable<string[]> with the string[] containing the keys checked.
One way could be:
const enabledFav$ = favOptions$.pipe(
map(resp => {
let result = [];
Object.keys(resp).forEach(key => {
if (resp.key) {
result.push(key);
}
});
return result;
})
);
I dont know the scenario but this is basically the code.
Here enabledFav$is Observable<string[]> and favOptions$ is Observable<FavoritesResponse>
I want to write a constructor function that takes some predefined methods from an object (Methods) and injects it into every new object that is created with the constructor. It injects methods from another object because, I want the consumers of my module to be able to add new methods.
But the problem is: as all the injected methods are not defined in the constructor I can't seem to manage their type annotations properly.
It's hard for me to describe the problem so I created a simple example (with JavaScript to avoid all the type error) to demonstrate it.
// methods.js ---------------------------------------
const methods = {};
const addMethod = (name, value) => (methods[name] = value);
// these methods should be added by an external user of the programmer.js module
function code(...task) {
this.addInstruction({
cmd: "code",
args: task
});
return this;
}
function cry(...words) {
this.addInstruction({
cmd: "cry",
args: words
});
return this;
}
addMethod("code", code);
addMethod("cry", cry);
// programmer.js -------------------------------------
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "VERY_SECRET_KEY";
function Programmer() {
const instructions = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key) {
if (key === secretKey) return instructions;
},
});
for (const key of Object.keys(methods))
this[key] = (...args) => methods[key].apply(this, args);
}
// test.js -------------------------------------------
const desperateProgrammer = new Programmer();
const instructions = desperateProgrammer
.code("A library in typescript within 10 days")
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!")
// the next two lines shouldn't work here (test.js) as the user
// shouln't have asscess to the "retriveInstructionsMethod" and "secretKey"
// keys. I'm just showing it to demonstrate what I want to achieve.
[retriveInstructionsMethod](secretKey);
console.log(instructions);
Here I want to hide all the instructions given to a Programmer object. If I hide it from the end user then I won't have to validate those instructions before executing them later.
And the user of programmer.js module should be able to add new methods to a programmer. For example:
// addMethods from "methods.js" module
addMethods("debug", (...bugs) => {...});
Now I know that, I can create a base class and just extend it every time I add a new method. But as it is expected that there will be lots of external methods so soon it will become very tedious for the user.
Below is what I've tried so far. But the type annotation clearly doesn't work with the following setup and I know it should not! Because the Methods interface's index signature([key: string]: Function) is very generic and I don't know all the method's name and signature that will be added later.
methods.ts
export interface Methods {
[key: string]: Function;
}
const methods: Methods = {};
export default function getMethods(): Methods {
return { ...methods };
}
export function addMethods<T extends Function>(methodName: string, method: T) {
methods[methodName] = method;
}
programmer.ts
import type { Methods } from "./methods";
import getMethods from "./methods";
export type I_Programmer = Methods & {
addInstruction: (arg: { cmd: string; args: unknown[] }) => void;
};
interface ProgrammerConstructor {
new (): I_Programmer;
(): void;
}
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "ACCESS_KEY";
const Programmer = function (this: I_Programmer) {
const instructions: object[] = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key: string) {
if (key === secretKey) return instructions;
},
});
const methods = getMethods();
for (const key of Object.keys(methods))
this[key] = (...args: unknown[]) => methods[key].apply(this, args);
} as ProgrammerConstructor;
// this function is just to demonstrate how I want to extract all the
// instructions. It should not be accessible to the end user.
export function getInstructionsFrom(programmer: I_Programmer): object[] {
// gives error
// #ts-expect-error
return programmer[retriveInstructionsMethod][secretKey]();
}
export default Programmer;
testUsages.ts
import { addMethods } from "./methods";
import type { I_Programmer } from "./programmer";
import Programmer, { getInstructionsFrom } from "./programmer";
function code(this: I_Programmer, task: string, deadline: string) {
this.addInstruction({ cmd: "code", args: [task, deadline] });
return this;
}
function cry(this: I_Programmer, words: string) {
this.addInstruction({ cmd: "cry", args: [words] });
return this;
}
addMethods("code", code);
addMethods("cry", cry);
const desperateProgrammer = new Programmer()
.code("a library with typescript", "10 days") // no type annotation of "code" method
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!"); // same here
// Just for demonstration. Should not be accessible to the end user!!!
console.log(getInstructionsFrom(desperateProgrammer));
Kindly give me some hint how I can solve this problem. Thanks in advance.
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));
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.
I'm going to explain the context I have before explain the problem design a service with Angular and RxJS.
I have one object with this model:
{
id: string;
title: string;
items: number[];
}
I obtain each object of this type (called "element") through GET /element/:id
Each element has an items: number[] that contains an array of numbers and each number has an URL so I do a new GET /element/:id/:itemNumber for each number and return the items detail. That item detail model is like:
{
idItem: string;
title: string;
}
So in my service I want to serve to the components a method that it will return an Observable, it will obtain an array of objects, where each object has the element model with one addition property that will be an array of its detailed items. So, I'm going to show what I have:
The service:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
map(element => this.getAllItemsDetail(element))
}
getAllItemsDetail(element: Element): any {
return of(...element.items).pipe(
map(val => this.getItemDetail(element.id, val)),
concatAll()
);
}
My problem is, I'm not understanding how I can, in RxJS, after the map(element => this.getAllItemsDetail(element)) in the getElement method, merge the items array returned by it and the previous element I have before the map that has the element object. I need to add "anything" after that map to compute an Object.Assign to merge the both objects.
EDIT
With the answer of #chiril.sarajiu, he gives me more than one clue, with switchMap I can map the observable (map is used for objects, "do a compute with the result object"). So my code, it's not the most pretty version I know..., I will try on it, but it works. So the final code looks like this:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element)),
map(result => { return Object.assign({}, result.element, {itemsDetail: result.items})})
);
}
getAllItemsDetail(element: Element): any {
const results$ = [];
for (let i = 0; i < element.items.length; i++) {
results$.push(this.getItemDetail(element.id, element.items[i]));
}
return zip(...results$).pipe(
map(items => { return Object.assign({}, { element, items })})
);
}
Be aware of the possibility about zip will not emit values if the results$ is empty, in that case you can add of(undefined) to the results$ array and after take into account that items will be [undefined]
To map another observable you should use switchMap function instead of map
So
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element))
}
UPDATE
Consider this medium article