Multiple asynchronous string replace in RxJS - javascript

In an Angular app I have:
a text in which I want to translate several resource keys
an array of resource keys ("IS_LESS_THAN", "IS_GREATER_THAN", etc...)
an array of languages ("EN, NL, FR", etc..)
a service that can translate a single resource key. The service facilitates that when the chosen language changes, it emits a new value through the observable.
Asynchronous is the complex part. I cannot do a simple string.replace() on the text with a for loop because that would only work in a synchronous way.
How can the translation be accomplished using RxJS?
I have the following code, but stuck at the end:
const text = '....... 3 IS_LESS_THAN 5 ........';
const keys = ['IS_LESS_THAN', 'IS_GREATER_THAN', ....... ];
const translatedText$ = of(keys).pipe(
switchMap((key) =>
this.localizationService.translate(key).pipe( // Will update its value when the current language changes
map((value) => ({
// Store the original key/value combination to do the replace
key,
value,
}))
)
// PROBLEM: How to replace each key in the text and return the result?
// I tried reduce, but how to combine the accumulator and the value?
reduce((acc, val) => {
return val[0].replace(val[1], );
})

You need to provide the initial seed value, which in your case is probably text.
reduce((translatedText, translation) => {
return translatedText.replace(translation.key, translation.value);
}, text)
You should also think about changing switchMap to mergeMap if you want all of your keys to be translated: https://ncjamieson.com/avoiding-switchmap-related-bugs/

It was easier than expected. The catch was to combine the translation results first, using combineLatest. From that point, all translations are available in a substitutions list in a single element of the observable. It is no longer needed to reduce multiple observable items:
return combineLatest(
keys.map((key) =>
this.localizationService.translate(key)))
).pipe(map((substitutions) => replace(text, substitutions)));

Related

How to properly flat this Observable array?

I need to flat the results of a Observable<Order[]>[] in a Order[].
The current way that I'm doing it:
const ordersObservable: Observable<Order[]>[] = [];
//ordersObservable is populated with a bunch of Observable<Order[]>
forkJoin(ordersObservable)
.pipe(
map((results) => ([] as Order[]).concat(...results))
)
.subscribe((orders: Order[]) => {
this.orderService.set(orders);
//...
});
I've been told that I shouldn't be using pipe like that and should be using a RxJs function to handle that.
I've tried to use concatAll and mergeAll instead of the map, but that ended in calling the orderService for every item in ordersObservable, instead a single time with a flat array with all results of ordersObservable.
What am I doing wrong? And how can I flat the results of all the observables in a single array, preferably with a native RxJs solution?
An Observable is meant to emit values. With an array of Observables, we need to subscribe to each element of the Observable array to emit the Order array.
I honestly think you should re-evaluate creating an array of Observables. If you want to post another question with the code that is generating that array of Observables, we could provide suggestions.
That said, I was able to get something to work. I didn't spend the time to see if there was an easier approach.
from(this.ordersObservable)
.pipe(
concatAll(),
scan((acc, value) => [...acc, ...value], [] as Order[]),
takeLast(1)
)
.subscribe((x) => console.log('result', JSON.stringify(x)));
First, I needed something to subscribe to. We can't subscribe to the array of Observables. So I used from to turn the Array of Observables into another Observable. That way I could subscribe and get the code to execute.
The from emits each Observable from the array.
NOTE: forkJoin and combineLatest also work instead of from and provide the same result.
I then use concatAll() to concatenate the inner Observables in sequence. It subscribes to each Observable in the array.
scan allows us to define an accumulator, accumulating each array of Orders into a single array of orders.
UPDATE: Added takeLast(1) to ensure only the last result is emitted.
UPDATE 2: Second option
This also worked and may be a bit simpler:
concat(...this.ordersObservable)
.pipe(
scan((acc, value) => [...acc, ...value], [] as Order[]),
takeLast(1)
)
.subscribe((x) => console.log('result', JSON.stringify(x)));
This uses array destructuring (the "...") with the concat operator to emit all of the values from all of the Observables in the array of Observables. Then we still use scan to accumulate them and takeLast(1) to emit only once.
I would be interested to know if anyone else is able to simplify this!

Prevent pushing to array if duplicate values are present

I'm mapping an array and based on data i'm pushing Option elements into an array as follows
let make_children: any | null | undefined = [];
buyerActivityResult && buyerActivityResult.simulcastMyAccount.data.map((item: { make: {} | null | undefined; }, key: any) => {
make_children.push(
<Option key={key}>{item.make}</Option>
);
});
Following data array has several objects and these objects have an attribute called model.
buyerActivityResult.simulcastMyAccount.data
I want to prevent pusing Options to my array if the attribute model has duplicate data. It only has to push once for all similar model values.
How can i do it?
I tried something like this
buyerActivityResult && buyerActivityResult.simulcastMyAccount.data.map((item: { model: {} | null | undefined; }, key: any) => {
model_children.indexOf(item.model) === -1 && model_children.push(
<Option key={key}>{item.model}</Option>
);
});
But still duplicate values are being pushed into my array.
Its difficult to tell what you are trying to achieve but it looks like a map may not be the right tool for the job.
A map returns the same sized length array as that of the original array that you are calling map on.
If my assumptions are correct, your buyerActivityResult.simulcastMyAccount.data array has duplicate values, and you want to remove these duplicates based on the model property? One way to achieve this would be to use the lodash library for this, using the uniq function:
const uniqueResults = _.uniq(buyerActivityResult.simulcastMyAccount.data, (item) => item.model);
The Array.prototype.map() method is supposed to be used for manipulating the data contained into the array performing the operation. To manipulate data from other variables I recommend to use a for-loop block.
If item.model is an object, the function Array.prototype.indexOf() always returns -1 because it compares the memory address of the objects and does not do a deep comparison of all properties values.
The usual solution to remove duplicate data from an array is converting the Array into a Set then back to an Array. Unfortunately, this works only on primary type values (string, number, boolean, etc...) and not on objects.
Starting here, I will review your source code and do some changes and explain why I would apply those changes. First of all, assuming the make_children array does not receive new attribution later in your code, I would turn it into a constant. Because of the initialization, I think the declaration is overtyped.
const make_children: any[] = [];
Then I think you try to do too much things at the same time. It makes reading of the source code difficult for your colleagues, for you too (maybe not today but what about in few weeks...) and it make testing, debugging and improvements nearly impossible. Let's break it down in at least 2 steps. First one is transforming the data. For example remove duplicate. And the second one create the Option element base on the result of the previous operation.
const data: { make: any }[] = buyerActivityResult?.simulcastMyAccount?.data || [];
let options = data.map((item) => !!item.model); // removing items without model.
// Here the hard part, removing duplicates.
// - if the models inside your items have a property with unique value (like an ID) you can implement a function to do so yourself. Take a look at: https://stackoverflow.com/questions/2218999/remove-duplicates-from-an-array-of-objects-in-javascript
// - or you can use Lodash library like suggested Rezaa91 in its answer
options = _.uniq(data, (item) => item.model);
Now you only have to create the Option elements.
for (var i = 0; i < options.length; i++) {
model_children.push(<Option key={i}>{options[i].model}</Option>);
}
// OR using the Array.prototype.map method (in this case, do not declare `model_children` at the beginning)
const model_children:[] = options.map((opt:any, i:number) => <Option key={i}>{opt.model}</Option>);
Despite the lack of context of the execution of the code you provided I hope my answer will help you to find a solution and encourage you to write clearer source code (for the sake of your colleagues and your future self).
PS: I do not know anything about ReactJs. forgive me my syntax mistakes.

Why rxjs print data one single character every time?

This is my code in RxJs6:
const observable$ = interval(1000).pipe(
take(2),
map(x => interval(1500).pipe(
map(y => x+':'+y),
take(2),
concatAll()
)),
);
observable$.subscribe(obs => {
obs.subscribe(x => console.log(x));
});
I expect my code show the result like this:
0:0
1:0
0:1
1:1
But it actually shows:
why my code print data only one character every time ? And I think it should work like what i expected above not the actual result. anything wrong i understand about rxjs ?
This is because of concatAll(). It's typically used to flatten nested Observables but it can work with Promises and arrays (array-like objects) as well. Ant this is exactly what you're seeing here.
It thinks you want to flatten an array even when you have a string so it takes each item in the array (character in your case) and reemits it separately.
However, another question is what you wanted to achieve with concatAll.

Filtering Observable array elements from another Observable array

I had an issue to filter an observable array, in my case it's resources$ (an observable which contains all 'resourses' as JSON), and I have another observable called usedResources$, what I want to achieve is simply get unusedResources$ from those 2 variables (resources - usedResources = unusedResources), is there any RxJS way to achieve this?
If you have multiple streams and you want to combine each item from them somehow, usually that means either combineLatest or zip depending on your desired combination strategy.
combineLatest | documentation
If you want to compute the latest from the most recent item from each stream, regardless of how fast or slow they emit relative to eachother, you would use combineLatest; either Observable.combineLatest or the prototype based stream$.combineLatest which has the same effect but includes the stream you call it on instead of being a static factory. I personally use the static form more often for clarity.
This is probably what you want.
const unusedResources$ = Observable.combineLatest(
resources$,
usedResources$,
(resources, usedResources) => ({
something: resources.something - usedResources.something
})
);
const { Observable } = Rx;
const resources$ = Observable.interval(5000).map(i => ({
something: (i + 1) * 1000
}));
const usedResources$ = Observable.interval(1000).map(i => ({
something: (i + 1) * 10
}));
const unusedResources$ = Observable.combineLatest(
resources$,
usedResources$,
(resources, usedResources) => ({
something: resources.something - usedResources.something
})
);
unusedResources$.subscribe(
unusedResources => console.log(unusedResources)
);
<script src="https://unpkg.com/rxjs#5.4.0/bundles/Rx.min.js"></script>
zip | documentation
If you instead want to combine each item 1:1 i.e. waiting for every stream to emit an item for a given index, you can use zip. However, under the hood it uses an unbounded buffer, so if your streams don't emit at around the same interval you can potentially balloon your memory usage or even run out entirely. For the most part, this should only be used for streams which have a finite, predictable count and interval. For example, if you made N number of ajax calls and want to combine the results of them 1:1.

Creating a filterable list with RxJS

I'm trying to get into reactive programming. I use array-functions like map, filter and reduce all the time and love that I can do array manipulation without creating state.
As an exercise, I'm trying to create a filterable list with RxJS without introducing state variables. In the end it should work similar to this:
I would know how to accomplish this with naive JavaScript or AngularJS/ReactJS but I'm trying to do this with nothing but RxJS and without creating state variables:
var list = [
'John',
'Marie',
'Max',
'Eduard',
'Collin'
];
Rx.Observable.fromEvent(document.querySelector('#filter'), 'keyup')
.map(function(e) { return e.target.value; });
// i need to get the search value in here somehow:
Rx.Observable.from(list).filter(function() {});
Now how do I get the search value into my filter function on the observable that I created from my list?
Thanks a lot for your help!
You'll need to wrap the from(list) as it will need to restart the list observable again every time the filter is changed. Since that could happen a lot, you'll also probably want to prevent filtering when the filter is too short, or if there is another key stroke within a small time frame.
//This is a cold observable we'll go ahead and make this here
var reactiveList = Rx.Observable.from(list);
//This will actually perform our filtering
function filterList(filterValue) {
return reactiveList.filter(function(e) {
return /*do filtering with filterValue*/;
}).toArray();
}
var source = Rx.Observable.fromEvent(document.querySelector('#filter'), 'keyup')
.map(function(e) { return e.target.value;})
//The next two operators are primarily to stop us from filtering before
//the user is done typing or if the input is too small
.filter(function(value) { return value.length > 2; })
.debounce(750 /*ms*/)
//Cancel inflight operations if a new item comes in.
//Then flatten everything into one sequence
.flatMapLatest(filterList);
//Nothing will happen until you've subscribed
source.subscribe(function() {/*Do something with that list*/});
This is all adapted from one of the standard examples for RxJS here
You can create a new stream, that takes the list of people and the keyups stream, merge them and scans to filter the latter.
const keyup$ = Rx.Observable.fromEvent(_input, 'keyup')
.map(ev => ev.target.value)
.debounce(500);
const people$ = Rx.Observable.of(people)
.merge(keyup$)
.scan((list, value) => people.filter(item => item.includes(value)));
This way you will have:
-L------------------ people list
------k-----k--k---- keyups stream
-L----k-----k--k---- merged stream
Then you can scan it. As docs says:
Rx.Observable.prototype.scan(accumulator, [seed])
Applies an accumulator function over an observable sequence and returns each
intermediate result.
That means you will be able to filter the list, storing the new list on the accumulator.
Once you subscribe, the data will be the new list.
people$.subscribe(data => console.log(data) ); //this will print your filtered list on console
Hope it helps/was clear enough
You can look how I did it here:
https://github.com/erykpiast/autocompleted-select/
It's end to end solution, with grabbing user interactions and rendering filtered list to DOM.
You could take a look at WebRx's List-Projections as well.
Live-Demo
Disclosure: I am the author of the Framework.

Categories

Resources