I have an Observable that has list of items. Each item has a selected$ (boolean) observable. I need to filter out items, that have non-truth values.
Here is my solution, it feels a bit cumbersome, but it does the job. Is there a better solution?
interface ICarrier {
readonly id: number;
readonly selected$: Observable<boolean>;
}
const allCarriers$ = of<readonly ICarrier[]>([]); // Placeholder value!
const selectedCarriers$ = allCarriers$.pipe(
switchMap(carriers => {
return combineLatest(carriers.map(carrier => carrier.selected$.pipe(
map(selected => selected ? carrier : null),
))).pipe(
// Prevent multiple filtering when multiple values changes at once.
debounceTime(0),
// filter out null values
map(carriers => carriers.filter((carrier): carrier is ICarreir => !!carrier)),
);
}),
);
Related
I have a code that fetches book by its id
const fetchBook = (bookId: number) => {
const title = 'Book' + bookId;
// mimic http request
return timer(200).pipe(mapTo({ bookId, title }));
}
const bookId$ = new Subject<number>();
const book$ = bookId$.pipe(
switchMap(bookId => fetchBook(bookId)),
shareReplay(1)
);
book$.subscribe(book => console.log('book title: ', book.title))
bookId$.next(1);
I have an API method that patches values and returns the updated object:
const patchBook = (bookId: number, newTitle: string) => {
return timer(200).pipe(mapTo({ bookId, title: newTitle }));
}
What should I do to get book$ to emit the new value after I call patchBook(1, 'New Book Title')?
I can declare book$ as Subject explicitly and update it manually. But it will be imperative, not reactive approach.
Upd: The patch is called as a result of user action at any time (or never)
Upd2: Actually book$ can be also changed on server side and my real code looks like this:
const book$ = combineLatest([bookId$, currentBookChangedServerSignal$]).pipe...
The same thing you did to transform a bookId into a Book, you can use to transform a Book into a patchBook.
const book$ = bookId$.pipe(
switchMap(bookId => fetchBook(bookId)),
mergeMap(({bookId, title}) => patchBook(bookId, title)),
shareReplay(1)
);
Update:
patch is not always called
There are many ways this could be done and the "best" way really depends on how you've architected your system.
Lets say you dynamically create a button that the user clicks and this triggers an update event.
const patchBtn = document.createElement("button");
const patchBook$ = fromEvent(patchBtn, 'click').pipe(
switchMap(_ => patchBook(bookId, title))
);
const basicBook$ = bookId$.pipe(
switchMap(bookId => fetchBook(bookId))
);
const book$ = merge(patchBook$, basicBook$).pipe(
shareReplay(1)
);
You probably want your fromEvent events to emit some data rather then hard-coding (bookId, title) into the stream from a click, but you get the idea. That's just one of many ways to get the job done.
And of course, it should almost always be possible (and desirable) to remove bookId$, and replace it with a more reactive-style mechanism that hooks declarativly into whatever/wherever the ID's come from in the first place.
You can declare a fetchBook$ observable, and a patchBook$ subject. Then your book$ observable can be a merge of the two.
const patchBook = (bookId: number, newTitle: string) => {
return timer(200).pipe(
mapTo({ bookId, title: newTitle }),
tap(newBook=>this.patchBook$.next(newBook))
);
}
const bookId$ = new Subject<number>();
const fetchBook$ = bookId$.pipe(
switchMap(bookId => fetchBook(bookId)),
shareReplay(1)
);
const patchBook$ = Subject<{ bookId: number, newTitle: string}>();
const book$ = merge(fetchBook$, patchBook$);
book$.subscribe(book => console.log('book title: ', book.title))
bookId$.next(1);
patchBook(2, 'Moby Dick');
I am new to Angular with Akita.
I have an application where the users are loaded and set in the store. all user have an initial imageUrl property set to eg: 'http://testxxxxxx'
The component query the store for all users, and pipe to calls a method which loops through each person, make an api call to get 'blob' image response from api, and update the store with the person's imageUrl = 'blob:http:2fk2fjadkf' and the component set the <img src='imageUrl'.
But for some reason the method inside the outer observable is looping for many times. not sure why.
Here is my code:
Component:
peopleToShow$ = this.peopleFacade.peopleToShow$;
Component.html uses peopleToShow$ and the imageUrl property of each person. Right now it is not taking the updated blob url which is set in the this.loadImagesAsBlobs
Facade:
peopleToShow$ = this.peopleQuery.peopleToShow$
.pipe(
tap((people) => this.loadImagesAsBlobs(people))
);
private loadImagesAsBlobs(people: Person[]) {
people.forEach((person) => {
if (!person.isUrlChecked) {
this.imageDataService
.getAndStoreImageOnClient(person.imageUrl)
.pipe(
take(1),
switchMap((safeUrl) => {
this.updatePersonWithBlobImageUrl(person.id, safeUrl);
return EMPTY;
}),
catchError(() => {
this.updatePersonWithBlobImageUrl(person.id, null);
return EMPTY;
})
)
.subscribe();
}
});
}
private updatePersonWithBlobImageUrl(id: number, blobUrl: SafeUrl) {
this.peopleStore.updatePersonWithBlobImageUrl(id, blobUrl as string);
}
Thanks
It's not within this code, but when I've had this problem, it was because I had multiple things listening to a single observable, which means it was all happening several times.
To fix, change
peopleToShow$ = this.peopleQuery.peopleToShow$
.pipe(
tap((people) => this.loadImagesAsBlobs(people))
);
to
peopleToShow$ = this.peopleQuery.peopleToShow$
.pipe(
tap((people) => this.loadImagesAsBlobs(people)),
share()
);
or use shareReplay(1) instead, if you're worried about peopleToShow$ emitting before everything downstream is set up.
I have a situation where I have an observable, and for each emitted item, I want to create another observable, but ignore that observable's value and instead return the result of the first observable.
For example, if I click a button, I want to track something that happens in another button, only when the first button is toggled on.
I can do this now, sort of, with a hack, by taking the output of the child observable and piping it to a mapTo with the parent's value. You can see it in this code, which can be played with in a code sandbox:
import { fromEvent, from } from "rxjs";
import { mapTo, switchMap, tap, scan } from "rxjs/operators";
const buttonA = document.getElementById("a");
const buttonB = document.getElementById("b");
const textA = document.querySelector('#texta');
const textB = document.querySelector('#textb');
fromEvent(buttonA, 'click').pipe(
// this toggles active or not.
scan((active) => !active, false),
switchMap(active => {
if (active) {
const buttonBClicks$ = fromEvent(buttonB, 'click');
// here we can observe button b clicks, when button a is toggled on.
return buttonBClicks$.pipe(
// count the sum of button b clicks since button a was toggled on.
scan((count) => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
}),
// ignore the value of the button b count for the final observable output.
mapTo(active)
)
} else {
textB.value = ``;
return from([active]);
}
})
).subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
A couple issues here. In the case that the button is toggled on, the outer observable only receives a value once the button is clicked.
This mapTo use seems hacky.
Any better ways to do this?
It sounds like you don't want the inner observable to actually be a part of the process at all. Are you waiting on it or anything?
If not, you can just do it all as a side effect as follows:
fromEvent(buttonA, 'click').pipe(
scan((active) => !active, false),
tap(active => { if(active) {
fromEvent(buttonB, 'click').pipe(
scan(count => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
})
).subscribe()
}})
).subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
Nested subscriptions are considered bad voodoo, so you ca refactor like this to keep your separation of conserns more apparent:
const trackActiveFromButton$ = fromEvent(buttonA, 'click').pipe(
scan((active) => !active, false),
shareReplay(1)
);
trackActiveFromButton$.subscribe({
next: buttonActive => {
textA.value = `Button a active: ${buttonActive}`
}
});
trackActiveFromButton$.pipe(
switchMap(active => active ?
fromEvent(buttonB, 'click').pipe(
scan(count => count+1, 0),
tap(buttonBCount => {
textB.value = `button b count ${buttonBCount}`;
})
) :
EMPTY
)
).subscribe();
Any better ways to do this?
The below may be better depending on your taste. It seems to me your sample code gets a little messy because you have a single observable that is trying to do too many things. And the side-effects are sort of mixed in with the stream behavior logic.
It's totally fine to be use tap() to do side-effect type things, but sometimes it can make it harder to follow. Especially in the above code, since there is a nested observable involved.
Creating separate observables that always emit specific data can make things easier to follow.
If we declare a stream to represent the isActive state and subscribe to that to update textA, and define a counter stream to represent the number of clicks that occurred while isActive = true, using that value to update textB, I think it makes it easier to follow what's going on:
const clicksA$ = fromEvent(buttonA, 'click');
const clicksB$ = fromEvent(buttonB, 'click');
const isActive$ = clicksA$.pipe(
scan(active => !active, false),
startWith(false)
);
const counterB$ = combineLatest([isActive$, clicksB$]).pipe(
scan((count, [isActive]) => isActive ? count + 1 : -1, 0)
);
counterB$.subscribe(
count => textB.value = count === -1 ? '' :`button b count ${count}`
);
isActive$.subscribe(
isActive => textA.value = `Button a active: ${isActive}`
);
To me, having the streams defined separately makes it easier to see the relationship between them, meaning, it's easier to tell when they will emit:
isActive derives from clicksA
counterB derives from clicksB & isActive
Here's a working StackBlitz
Also:
the outer observable only receives a value once the button is clicked
This can be solved using startWith() to emit a default value.
How do I filter an observable using values from another observable, like an inner join in SQL?
class Item {
constructor(public name: string public category: string) {
}
}
class NavItem {
constructor(public key: string public isSelected: boolean = false) {
}
}
// Build a list of items
let items = of(new Item('Test', 'Cat1'), new Item('Test2', 'Cat2'))
.pipe(toArray());
// Determine the unique categories present in all the items
let navItems = from(items)
.pipe(mergeAll(),
distinct((i:item) => i.category),
map(i=>new NavItem(i.category)),
toArray());
I'm building a faceted search so let's say that in the UI, the "Cat1" NavItem is selected, so I want to produce an observable of all the items that have that category. After filtering down to the selected NavItem, I'm not sure how to bring in the Items, filter them down and spit out only those Items that mach a selected category. Here's what I have:
let filteredItems = navItems.pipe(
mergeAll(),
filter(n => n.isSelected))
// join to items?
// emit only items that match a selected category?
Expected result would be
[{name: 'Test', category: 'Cat1'}]
If I understand correctly, you want to select a certain navItem representing a certain category and then select all the items which have such category.
If this is true, than you can consider to create a function or method such as this
selectedItems(categoryId) {
return items.pipe(filter(item => item.category === categoryId));
}
Once you click the navItem then you raise an event which references the navItem and therefore the category id you are interested. You have then to call selectedItems function and pass the category id you selected. This will return an Observable which emits all the items of the category you have selected.
I finally got back around to looking at this and I figured out how to solve this. I needed to use switchMap to join the observables together in the same context. Then I can use map to emit all relevant items:
let filteredItems = from(items)
.pipe(mergeAll(),
switchMap((i) => navItems
// Get only navItems that are selected
.pipe(filter(ni => ni.isSelected),
// Convert back to array so I can if there are any selected items
// if zero, then show all items
toArray(),
map((nav) => {
if(nav.lengh > 0) {
// If the item's category matches, return it
return nav.some(x => x.key == i.category) ? i : null;
}
// If no categories were selected, then show all
return i;
})
}),
// Filter out the irrelevant items
filter(x => x !== null),
toArray()
);
I have a state object with string keys and values. Events are coming in, containing key-value pairs to change the state.
I need a debounced stream that:
validates the events and drop all modifications in the debounce cycle if they lead to an invalid state
outputs the diff to the last valid state
For example, for the initial state of {k1: "v1"}, and an event of {k2: "v2"}, output {k2: "v2"}.
But for the events: {k3: "v3"} and {k4: "invalid"}, drop both changes. So when a new event {k5: "v5"} comes in, the k3 key is still undefined.
I was able to implement it, but only by using a new Subject that keeps track of the last valid state: (jsfiddle)
const lastValidState = new Rx.Subject();
const res = modifications
.buffer(debounce)
.withLatestFrom(lastValidState.startWith(state))
.map(([mods, last]) => {
// calculate next state
return [Object.assign({}, last, ...mods), last];
}).filter(([newState]) => {
// check new state
return Object.keys(newState).every((k) => !newState[k].startsWith("invalid"));
// update Subject
}).do(([newState]) => lastValidState.next(newState)).share()
.map(([newState, last]) => {
// output diff
return Object.assign({}, ...Object.keys(newState).filter((k) => newState[k] !== last[k]).map((k) => ({[k]: newState[k]})))
}
)
This code works well, but I don't like the new Subject it introduces. I would prefer a solution that does not rely on that and use only RxJS operators.
I've tried to use pairwise, but I could not figure out how to pair a stream with the last value of itself.
Thanks to cartant's comment, using scan is the way to go.
The only trick is to use distinctUntilChanged to prevent emits for invalid changes.
The modified code: jsfiddle
const res = modifications
.buffer(debounce)
.scan((last, mods) => {
const newState = Object.assign({}, last, ...mods);
const valid = Object.keys(newState).every((k) => !newState[k].startsWith("invalid"));
if (valid) {
return Object.assign({}, ...Object.keys(newState).filter((k) => newState[k] !== last[k]).map((k) => ({[k]: newState[k]})));
}else {
return last;
}
}, state)
.distinctUntilChanged()