angular and RxJS/switchMap - rxjs

Hello I'm new to RxJS and I'm just getting to know operators. I want to show in console next 6 numbers in one-second time interval after button click. I want to reset that counter after next click using switchMap.
I've been trying to do with switchMap, but counter is not reseting.
obsSwitchMap: Observable<any>;
this.obsSwitchMap = of(null).pipe(
switchMap(x => from([1, 2, 3]).pipe(
concatMap(item => of(item).pipe(delay(300)))
)
)
)
onSwitchMapBtnClick() {
this.obsSwitchMap.subscribe(x => {
console.log(x)
})
}
Numbers are displaying independently of each other

Although you want to learn, I think you should learn with the best practices from the start.
And it means you can do it very simply without switchMap :
const newInterval = () => rxjs.timer(0, 1000).pipe(
rxjs.operators.map(nb => new Array(6).fill(nb).map((v, i) => v + i + 1))
);
let subscription;
function resetTimer() {
subscription && subscription.unsubscribe();
subscription = newInterval().subscribe(v => console.log(v));
}
resetTimer();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js"></script>
<button onclick="resetTimer()">Reset timer</button>
EDIT
Here is a switchMap example :
const horsemen = [
{ id: 1, name: 'Death' },
{ id: 2, name: 'Famine' },
{ id: 3, name: 'War' },
{ id: 4, name: 'Conquest' },
];
// Fake HTTP call of 1 second
function getHorseman(id) {
return rxjs
.of(horsemen.find(h => h.id === id))
.pipe(rxjs.operators.delay(1000));
}
const query = document.querySelector('input');
const result = document.querySelector('div.result');
// Listen to input
rxjs.fromEvent(query, 'input')
.pipe(
rxjs.operators.map(event => +event.target.value), // Get ID
rxjs.operators.switchMap(id => getHorseman(id)) // Get Horseman
).subscribe(horseman => {
let content;
if (horseman) content = `Horseman = ${horseman.name}`;
else content = `Horseman unknown`;
result.innerText = content;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js"></script>
<input type="text" placeholder="Input an ID here (1-4)">
<div class="result"></div>

I have found simply solution using switchMap. On every click, restart your observable counter and take how many items you want.
const btn = document.querySelector('button');
fromEvent(btn, 'click').pipe(
switchMap((item => interval(1000).pipe(take(6)))),
).subscribe(console.log)

Related

NgRx effect call API and dispatch action in a loop

Hellow,
I have below Json structure, which is provided as a payload in the UpdateOrders action.
In the effect, I would like to iterate over the reservations and orders, call the this.orderApiService.updateOrder service and dispatch a UpdateOrderProgress action. In the UpdateOrderProgress action I would like to provide the numberOfReservationsUpdated and the totalReservations
const reservationOrders = [
{
reservationNumber: '22763883',
orders: [
{
orderId: 'O12341',
amount: 25
},
{
orderId: 'O45321',
amount: 50
}
]
},
{
reservationNumber: '42345719',
orders: [
{
orderId: 'O12343',
amount: 75
}
]
}
];
I have the following effect to achieve this, but unfortunately, this effect does not work and throws an exception.
#Effect()
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
return combineLatest(action.reservationOrders.map((x, index) => {
const totalReservations = action.reservationOrders.length;
const numberOfReservationsUpdated = index + 1;
return combineLatest(x.orders.map((order) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => [new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)]),
catchError((message: string) => of(console.info(message))),
);
}))
}))
})
);
How can I achieve this? Which RxJs operators am I missing?
Instead of using combineLatest, you may switch to using a combination of merge and mergeMap to acheive the effect you're looking for.
Below is a representation of your problem statement -
An action triggers an observable
This needs to trigger multiple observables
Each of those observables need to then trigger some
action (UPDATE_ACTION)
One way to achieve this is as follows -
const subj = new Subject<number[]>();
const getData$ = (index) => {
return of({
index,
value: 'Some value for ' + index,
}).pipe(delay(index*1000));
};
const source = subj.pipe(
filter((x) => !!x),
exhaustMap((records: number[]) => {
const dataRequests = records.map((r) => getData$(r));
return merge(dataRequests);
}),
mergeMap((obs) => obs)
);
source.subscribe(console.log);
subj.next([3,1,1,4]); // Each of the value in array simulates a call to an endpoint that'll take i*1000 ms to complete
// OUTPUT -
// {index: 1, value: "Some value for 1"}
// {index: 1, value: "Some value for 1"}
// {index: 3, value: "Some value for 3"}
// {index: 4, value: "Some value for 4"}
Given the above explaination, your code needs to be changed to something like -
const getOrderRequest$ = (order: OrderUpdateRequest, numberOfReservationsUpdated, totalReservations) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)),
catchError((message: string) => of(console.info(message))),
);
}
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
const reservationOrders = action.reservationOrders;
const totalLen = reservationOrders.length
const allRequests = []
reservationOrders.forEach((r, index) => {
r.orders.forEach(order => {
const req = getOrderRequest$(order, index + 1, totalLen);
allRequests.push(req);
});
});
return merge(allRequests)
}),
mergeMap(obs=> obs)
);
Side Note - While the nested observables in your example may work, there are chances that you'll be seeing wrong results due to inherent nature of http calls taking unknown amount of time to complete.
Meaning, the way you've written it, there are chances that you can see in some cases that numberOfReservationsUpdated as not an exact indicative of actual number of reservations updated.
A better approach would be to handle the state information in your reducer. Basically, pass the reservationNumber in the UPDATE action payload and let the reducer decide how many requests are pending completion. This will be an accurate representation of the state of the system. Also, it will simplify your logic in #effect to a single nested observable rather than multiple nesting.

Keep state while operating in switchMap

Suppose that you have a function that returns an rxjs observable that contains a list of objects.
const getItems = () =>
of([
{
id: 1,
value: 10
},
{
id: 2,
value: 20
},
{
id: 3,
value: 30
}
]);
and a second function that returns an observable with a single object
const getItem = id =>
of({
id,
value: Math.floor(Math.random() * 30) + 1
});
Now we want to create an observable that will get the first list and at a regular interval will randomly update any list item.
const source = getItems().pipe(
switchMap(items =>
interval(5000).pipe(
switchMap(x => {
// pick up a random id
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
)
);
})
)
)
);
source.subscribe(x => console.log(JSON.stringify(x)));
The problem with the above code is that each time the interval is triggered the items from the previous iteration reset to their initial form. e.g,
[{"id":1,"value":10},{"id":2,"value":13},{"id":3,"value":30}]
[{"id":1,"value":10},{"id":2,"value":20},{"id":3,"value":18}]
[{"id":1,"value":10},{"id":2,"value":16},{"id":3,"value":30}]
[{"id":1,"value":21},{"id":2,"value":20},{"id":3,"value":30}]
As you see, on each interval our code is resetting the list and updates a new item (eg value 13 is lost in the second iteration and reverts to 20).
The behaviour seems reasonable since the items argument in the first switchMap acts like a closure.
I managed to somehow solve the issue by using BehaviorSubject but i think that my solution is somehow dirty.
const items$ = new BehaviorSubject([]);
const source = getItems().pipe(
tap(items => items$.next(items)),
switchMap(() =>
interval(5000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items$
.getValue()
.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
),
tap(items => items$.next(items)),
switchMap(() => items$)
);
})
)
)
);
Is there a better approach ?
Example code can be found here
I believe this should be doing what you want:
const source = getItems().pipe(
switchMap(items =>
interval(1000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId);
}),
scan((acc, item) => {
acc[acc.findIndex(i => i.id === item.id)] = item;
return acc;
}, items),
)
)
);
It's basically what you're doing but I'm using scan (that is initialized with the original items) to keep the output array in acc so I can update it later again.
Live demo: https://stackblitz.com/edit/rxjs-kvygy1?file=index.ts

How to setState of array of Object value and toggle the count to either 1 or 2 on click

I want to update the state of value in an array of objects on click, the value will toggle between 1 and 2, once clicked if the existing value is 1 it will update it 2 on click, and if 2 it will update it 1. The value must change for the clicked object only and not all objects.
import React, {useRef, useState} from 'react'
import {BsThreeDots, BsBookmark, BsBookmarkFill} from 'react-icons/bs'
export const TextQuoteCard = () => {
const [textQuote, setTextQuote] = useState([
{
userId: '123',
userName: 'sample',
userImageUrl: 'https://qph.fs.quoracdn.net/main-thumb-ti-406-100-gtkoukgmmuzegaytlmtaybgrsoihzyii.jpeg',
quoteId: 'TQ119',
postDateTime: '28 June at 8:20',
quoteAuthorId: '123',
quoteAuthorName: 'john len',
quoteCategory: 'Motivational',
quoteType: 'textQuote',
quoteText: 'If there’s no market, then it may not be the best thing to do. Entrepreneurship is about finding market opportunities, or creating opportunities. If there’s no market, then you need to grow one',
quoteImageUrl: '',
// 1 = yes, 2 = no
bookmarkStatus: 1,
likesCount: 3300,
commentsCount: 123,
overallShareCount: 1203,
fbShareCount: 423,
twtShareCount: 1232,
waShareCount: 1023,
viewCount: 1923,
isSelected: null
}
])
const handleBookmark = i => {
let bookmarkStatus = [...textQuote]
let bookmark = bookmarkStatus[i].bookmarkStatus
console.log('before update' , bookmark)
if(bookmark === 1) {
bookmark = 2
} else if(bookmark === 2){
bookmark = 1
}
setTextQuote(bookmarkStatus)
console.log('after update', bookmark)
}
return(
<div>
{
textQuote.map((quote, index) => (
<div className="QuoteCardPrimaryContainer" key={quote.quoteId}>
className="QuoteCardAuthorFollowButtonActionContainer">
<span className="QuoteCardAuthorFollowButtonActionSpan"
onClick={() => handleBookmark(index)}>
<span className={quote.bookmarkStatus === 1 ?
'bookmarkButtonContainer activeBookmark':
'bookmarkButtonContainer'}>
{quote.bookmarkStatus === 1 ? <BsBookmarkFill/> :
<BsBookmark/>}
</span>
</span>
</div>
))
}
</div>
)
}
First get the object at that index where the bookmarkStatus has to be updated. Then using splice method you can replace with the updated object.
const handleBookmark = i => {
let quoteObj = {...textQuote[i]};
let bookmark = quoteObj.bookmarkStatus;
console.log('before update', bookmark);
if (bookmark === 1) {
quoteObj.bookmarkStatus = 2;
} else if (bookmark === 2) {
quoteObj.bookmarkStatus = 1;
}
textQuote.splice(i, 1, quoteObj)
console.log(textQuote);
setTextQuote([...textQuote]);
console.log('after update', textQuote[i].bookmarkStatus);
};
Hope this helps.

UI Flickers when I drag and drop and item

I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.

RXJS split by id and process in sequence for each id

Problem: Game: So I have some ships that can arrive to many planets. If the 2 ships arrive at the same time on the new planet can lead to the same process of changing ownership twice. This process is asynchronous and should only happen once per planet ownership change.
To fix this I want split the stream of ships by planet id so each stream will be for only one planet. Now the tricky part is that each ship should only be processed after the previous one has been processed.
Ships$
Split by planet id
planet id1: process in sequence
planet id2: process in sequence
...
Here is some code that will show how it should behave.
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
},
// ... never finishes
]
// the source observable never finishes
const source$ = interval(1000).pipe(
take(ships.length),
map(i => ships[i]),
)
const createSubject = (ship) => {
// Doesn't need to be a subject, but needs to emit new items after a bit of time based on some other requests.
console.log(`>>>`, ship.id);
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' a' + new Date());
}, 1000);
setTimeout(() => {
subject.next(ship.id + ' b' + new Date());
subject.complete();
}, 2000);
return subject.asObservable();
}
// The result should be the following (t, is the time in seconds, t3, is time after 3 seconds)
// t0: >>> 1
// t0: >>> 3
// t1: 1 a
// t1: 2 a
// t2: 1 b
// t2: 2 b
// t2: >>> 2 (note that the second ship didn't call the createSubject until the first finished)
// t3: 1 a
// t4: 1 2
Solution (with a lot of help from A.Winnen and some figuring out)
Run it here: https://stackblitz.com/edit/angular-8zopfk?file=src/app/app.component.ts
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
//subject.next(ship.id + ' b');
}, 500);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 1000);
return subject.asObservable();
}
let x = 0;
interval(10).pipe(//
take(ships.length),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group$ => {//
x++
return group$.pipe(
tap(i => console.log('x', i, x)),
concatMap(createSubject)
)
}),
).subscribe(res => console.log('finish', res), undefined, () => console.log("completed"))
How can this be done in rxjs?
Code:
const shipArriveAction$ = action$.pipe<AppAction>(
ofType(ShipActions.arrive),
groupBy(action => action.payload.ship.toPlanetId),
mergeMap((shipByPlanet$: Observable<ShipActions.Arrive>) => {
return shipByPlanet$.pipe(
groupBy(action => action.payload.ship.id),
mergeMap((planet$) => {
return planet$.pipe(
concatMap((action) => {
console.log(`>>>concat`, new Date(), action);
// this code should be called in sequence for each ship with the same planet. I don't need only the results to be in order, but also this to be called in sequence.
const subject = new Subject();
const pushAction: PushAction = (pushedAction) => {
subject.next(pushedAction);
};
onShipArriveAction(state$.value, action, pushAction).then(() => {
subject.complete();
});
return subject.asObservable();
}),
)
})
);
)
;
The code from A.Winnen is very close, but only works with a source observable that is finished, not continuous:
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' b');
}, 1000);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 2000);
return subject.asObservable().pipe(
finalize(null)
);
}
interval(1000).pipe(
take(ships.length),
tap(console.log),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group => group.pipe(toArray())),
mergeMap(group => from(group).pipe(
concatMap(createSubject)
))
).subscribe(res => console.log(res), undefined, () => console.log("completed"))
you can use a combination of groupBy and mergeMap to achieve your goal.
from(ships).pipe(
groupBy(ship => ship.planetId),
mergeMap(planetGroup => planetGroup.pipe(
concatMap(ship => {
// do real processing in this step
return of(`planetGroup: ${planetGroup.key} - processed ${ship.ship}`);
})
))
).subscribe(result => console.log(result));
I made a simple example: https://stackblitz.com/edit/angular-6etaja?file=src%2Fapp%2Fapp.component.ts
EDIT:
updated blitzstack: https://stackblitz.com/edit/angular-y7znvk

Resources