Conditional observables with forkJoin - javascript

I have a case where I may or may not need to add observables to a list. I then want to forkJoin the observables I do have so the page can load once all of the data is available.
let observables: Observable<any>[] = [];
observables.push(this.taskService.getStep(this.housingTransactionId, this.task.stageReferenceId, this.task.stepReferenceId));
if (this.task.associatedChatThreadId) {
observables.push(this.messageHubService.getChatThread(this.housingTransactionId, this.task.associatedChatThreadId));
}
if (this.task.associatedDocuments && this.task.associatedDocuments.length > 0) {
this.task.associatedDocuments.forEach(documentId => {
observables.push(this.documentHubService.getDocumentProperties(this.housingTransactionId, documentId));
});
}
Observable.forkJoin(observables)
.subscribe(([step, chatThread, ...documents]) => {
this.step = step;
this.chatThread = chatThread;
this.documents = documents;
this.isPageLoading = false;
}, error => {
this.isPageLoading = false;
console.log(error);
});
The problem I'm getting is that if I don't have a this.task.associatedChatThreadId, then the observable is not added to the list and when the forkJoin is executed, the ...documents are in the position of the chatThread property in the subscribe method (well, the first document!).
Is there a way to ensure the positioning of the responses from a forkJoin? Or should I/can I use a different approach?

Most easily you can add a dumb Observable.of(null) with null value if the condition is not met in order to keep the same order of responses:
if (this.task.associatedChatThreadId) {
observables.push(this.messageHubService....);
} else {
observables.push(Observable.of(null))
}
Then in the subscription you can check if chatThread === null becauese it'll always be present at the same position.
Alternatively, you could wrap each Observable in observables with some extra object that would make it uniquely identifiable in the subscriber but that would be unnecessarily complicated so I'd personally stick to the first option.

Another approach would be not to use folkJoin but subscribe separately. At the same time, make isPageLoading a BehaviorSubject which counts how many async requests you currently have. Each time when you make a request, you can have isPageLoading.next(1), and isPageLoading.next(-1) when you finish a request.

You could make a helper function that accepts an object which has string keys and observable values and returns an observable that will emit an object with the same keys, but having the resulting values instead of the observables as values.
I would not really say that this is a cleaner version than using of(null) like suggested by martin, but it might be an alternative.
function namedForkJoin(map: {[key: string]: Observable<any>}): Observable<{[key: string]: any}> {
// Get object keys
const keys = Object.keys(map);
// If our observable map is empty, we want to return an empty object
if (keys.length === 0) {
return of({});
}
// Create a fork join operation out of the available observables
const forkJoin$ = Observable.forkJoin(...keys.map(key => map[key]))
return forkJoin$
.map(array => {
const result = {};
for (let index = 0; index < keys.length; index++) {
result[keys[index]] = array[index];
}
}));
}
Please keep in mind, I did not have angular or rxjs running here at the moment, so I could not verify the function really works. But the idea is:
1. Get the keys from the input map.
2. Use the keys to get an array of observables and pass that to fork join.
3. Add a mapping function that converts the resulting array back into an object.

Related

How to prevent duplicate data from for loop

I am wondering what the best way is to prevent duplicate data from getting into a new array. I have a service call that returns the same array 3 times. I'm trying to take a number from inside the objects in the array and add them up to create a "total" number (fullRentAmt), but since the array gets returned 3 times I'm getting the total*3. I am thinking maybe .some() or .filter() could be of use here but I've never used those/am not sure how that would be implemented here. Thanks for any help!
What I tried, but it's not working/the new array isn't getting populated:
Component
properties = [];
fullRentAmt: number = 0;
const propertyDataSub = this.mainService.requestPropertyData()
.subscribe((pData: PropertyData[]) => {
if (pData) {
const propertyData = pData;
for (let i = 0; i < propertyData.length; i++) {
if (this.properties[i].propertyId !== propertyData[i].propertyId) {
this.properties.push(propertyData[i]);
}
}
for (let i = 0; i < this.properties.length; i++) {
this.fullRentAmt += this.properties[i].tenancyInformation[0].rentAmount;
}
});
Data returned from backend (array of 2 objects):
[
{
"tenantsData":[
{
"email":null,
"tenantNames":null,
"propertyId":2481,
}
],
"tenancyInformation":[
{
"id":2487,
"rentAmount":1000,
}
],
},
{
"tenantsData":[
{
"email":null,
"tenantNames":null,
"propertyId":3271,
}
],
"tenancyInformation":[
{
"id":3277,
"rentAmount":1200,
}
],
},
I'm not an angular developer, but I hope my answer will help you.
let the for loop duplicate the data as much as it wants. you just have to change the idea of storing the stuff from an array to a JavaScript Set
basically, it's very similar to arrays they're lists and iteratables that are very similar to arrays, the only difference is that they don't allow duplication,
usage:
const properties = new Set()
properties.add("yellow")
properties.add("blue")
properties.add("orange")
console.log(properties) // yellow, blue, orange
properties.add("blue")
properties.add("blue")
properties.add("blue")
console.log(properties) // yellow, blue, orange
after your for loop finishes, you may want to convert this set into a normal array, all you have to do is to use destructuring:
const propertiesArray = [...properties]
#YaYa is correct. I added this to show the correct code in Angular
properties = [];
fullRentAmt: number = 0;
const propertyDataSub = this.mainService.requestPropertyData()
.subscribe((pData: PropertyData[]) => {
if (pData && pData.length) {
let arrSet = new Set()
const propertyData = pData;
for (let i = 0; i < propertyData.length; i++) {
if (this.properties[i].propertyId !== propertyData[i].propertyId) {
arrSet.add(propertyData[i])
}
}
this.properties = Array.from(arrSet);
for (let i = 0; i < this.properties.length; i++) {
this.fullRentAmt += this.properties[i].tenancyInformation[0].rentAmount;
}
});
First thing you need to do is to fix your server and return the list once.
If server is out of your reach, you can leverage distinctUntilChanged pipe in combination with isEqual method in the frontend. You can either implement it yourself, or use a library such as lodash.
Also you do not have to subscribe, use async pipe in the template.
this.properties$ = this.mainService.requestPropertyData()
.pipe(
distinctUntilChanged(isEqual) // provide isEqual function somehow
);
this.totalRentAmount$ = properties$.pipe(
map(getTotalRentAmount)
);
// maybe in some other utility file:
export const getTotalRentAmount = (properties: Property[]): number => {
return properties
.map(property => property.tenancyInformation.rentAmount)
.reduce((total, amount) => total + amount, 0);
}
Then in the template:
<div>Total Rent Amount: {{ totalRentAmount | async }}</div>
Also if you really need to subscribe in the component and are only interested in the first emitted value of an observable, you can use first() or take(1) pipe to automatically unsubscribe after first value.
this.mainService.requestPropertyData()
.pipe(
first() // or take(1)
)
.subscribe(properties => this.properties = properties);
See the difference between first() and take(1)

Filter an Array of Objects from an Array in TypeScript

I built a custom component that filters an array of objects. The filter uses buttons, sets from active to non-active and allows more than one option on/off at the same time.
StackBlitz of my attempt - https://stackblitz.com/edit/timeline-angular-7-ut6fxu
In my demo you will see 3 buttons/options of north, south and east. By clicking on one you make it active and the result should include or exclude a matching "location" either north, south and east.
I have created my methods and structure to do the filtering, I'm struggling with the final piece of logic.
So far I have created a method to create an array of filtered locations depending on what the user clicks from the 3 buttons.
Next this passes to my "filter array" that gets the logic that should compare this filtered array against the original to bring back the array of results that are still remaining.
Its not quite working and not sure why - I originally got this piece of functionality working by using a pipe, but fore reasons do not want to go in that direction.
//the action
toggle(location) {
let indexLocation = this.filteredLocations.indexOf(location);
if (indexLocation >= 0) {
this.filteredLocations = this.filteredLocations.filter(
i => i !== location
);
} else {
this.filteredLocations.push({ location });
}
this.filterTimeLine();
}
// the filter
filterTimeLine() {
this.filteredTimeline = this.timeLine.filter(x =>
this.contactMethodFilter(x)
);
}
//the logic
private contactMethodFilter(entry) {
const myArrayFiltered = this.timeLine.filter(el => {
return this.filteredLocations.some(f => {
return f.location === el.location;
});
});
}
https://stackblitz.com/edit/timeline-angular-7-ut6fxu
Sorry for my expression but u have a disaster in your code. jajaja!. maybe u lost that what u need but the logic in your functions in so wrong. comparing string with objects. filter a array that filter the same array inside... soo u need make a few changes.
One:
this.filteredLocations.push({location});
Your are pushing object. u need push only the string.
this.filteredLocations.push(location);
Two:
filterTimeLine() {
this.filteredTimeline = this.timeLine.filter(x =>
this.contactMethodFilter(x)
);
}
in this function you filter the timeLine array. and inside of contactMethodFilter you call filter method to timeLine again....
See a functional solution:
https://stackblitz.com/edit/timeline-angular-7-rg7k3j
private contactMethodFilter(entry) {
const myArrayFiltered = this.timeLine.filter(el => {
return this.filteredLocations.some(f => {
return f.location === el.location;
});
});
}
This function is not returning any value and is passed to the .filter
Consider returning a boolean based on your logic. Currently the filter gets undefined(falsy) and everything would be filtered out

Javascript's method forEach() creates array with undefined keys

I am building a simple todo app, and I'm trying to get the assigned users for each task. But let's say that in my database, for some reason, the tasks id starts at 80, instead of starting at 1, and I have 5 tasks in total.
I wrote the following code to get the relationship between user and task, so I would expect that at the end it should return an array containing 5 keys, each key containing an array with the assigned users id to the specific task.
Problem is that I get an array with 85 keys in total, and the first 80 keys are undefined.
I've tried using .map() instead of .forEach() but I get the same result.
let assignedUsers = new Array();
this.taskLists.forEach(taskList => {
taskList.tasks.forEach(task => {
let taskId = task.id;
assignedUsers[taskId] = [];
task.users.forEach(user => {
if(taskId == user.pivot.task_id) {
assignedUsers[taskId].push(user.pivot.user_id);
}
});
});
});
return assignedUsers;
I assume the issue is at this line, but I don't understand why...
assignedUsers[taskId] = [];
I managed to filter and remove the empty keys from the array using the line below:
assignedUsers = assignedUsers.filter(e => e);
Still, I want to understand why this is happening and if there's any way I could avoid it from happening.
Looking forward to your comments!
If your taskId is not a Number or autoconvertable to a Number, you have to use a Object. assignedUsers = {};
This should work as you want it to. It also uses more of JS features for the sake of readability.
return this.taskLists.reduce((acc, taskList) => {
taskList.tasks.forEach(task => {
const taskId = task.id;
acc[taskId] = task.users.filter(user => taskId == user.pivot.task_id);
});
return acc;
}, []);
But you would probably want to use an object as the array would have "holes" between 0 and all unused indexes.
Your keys are task.id, so if there are undefined keys they must be from an undefined task id. Just skip if task id is falsey. If you expect the task id to possibly be 0, you can make a more specific check for typeof taskId === undefined
this.taskLists.forEach(taskList => {
taskList.tasks.forEach(task => {
let taskId = task.id;
// Skip this task if it doesn't have a defined id
if(!taskId) return;
assignedUsers[taskId] = [];
task.users.forEach(user => {
if(taskId == user.pivot.task_id) {
assignedUsers[taskId].push(user.pivot.user_id);
}
});
});
});

Finding elements in an array of objects where you have a bigger key

I'm writing a tiny reactive framework where I need to find out which subscriber needs updating. I'm implementing deep binding and I'm running into a wall how to find subscribers in an effective manner.
A stored variable can be an object, so for example
{
"user": {
"preferences": {
"food": "vegetarian"
}
}
}
You can get content to any level of this variable like this
getVar("user_preferences_food");
getVar("user_preferences");
However, you can also update it like that
setVar("user_preferences_food", "meat");
setVar("user_preferences", {"food": "meat"});
But in case of the first setVar (user_preferences_food) how can I find the subscriber using getVar("user_preferences"); or even getVar("user"); most effectively.
I already got it working by splitting the var on _ and then one by one concatting the next level and merging all the resulting arrays. But this is very resource intensive. Especially if there are a lot of subscribers. There must be a better way to find them that is less resource intensive.
Edit: I left out part of the explanation.
There is a subscribe method too
subscribe("user", cb);
subscribe("user_preferences", cb);
subscribe("user_preferences_food", cb);
These subscriptions are stored in an array in the framework.
As soon as "user_preferences_food" is updated for example, all subscriptions above should be triggered. But obviously not subscribe('othervar');
simplification of the subscribe method:
var subscriptions = [];
function subscribe(var, callback){
subscriptions.push({var: var, cb: callback});
}
Simplification of getVar
vars = {};
getVar(var){
// find var in vars with this logic: https://stackoverflow.com/a/18937118/249710
// current exact match on subscribers, but need the "parents, grandparents etc here
var toUpdate = _.where(subscriptions, {
"var" : var
});
_.each(toUpdate, function(sub){ sub.cb();});
}
Storing or getting data as part of the key I've already got covered. It is just finding the subscribers in the most effective manner
ps: this is in an environment where I cannot rely on ES6 yet (not all users have it enabled), there is no DOM but I do have underscore included. (Titanium app development platform)
I would try to make a list for the callbacks, so you loop trough one list so you dont have to search, because you know the list is there with all the callbacks.
So if you call setVar('user_prefs') you set a seperate list with the root var. in this case its the user.
if any object is changed with setVar (in depth or not) you go to you're root var, get the list and loop trough this list with the callbacks.
The beauty of this is you can set a list with the root var,
var cbList[FIRSTVAR] this contains all the callbacks. No searching just loop.
Its the mongoDb principle, the data is ready to go, you don't search because you know the list is already there.
You could split the string and use it for reduceing the object.
function getVar(object, path) {
return path
.split('_')
.reduce(function (o, k) {
return (o || {})[k];
}, object);
}
function setVar(object, path, value) {
var keys = path.split('_'),
last = keys.pop();
keys.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = value;
}
var object = { user: { preferences: { food: "vegetarian" } } };
console.log(getVar(object, "user_preferences_food"));
console.log(getVar(object, "user_preferences"));
setVar(object, "user_preferences_food", "meat");
console.log(object);
setVar(object, "user_preferences", {"food": "meat"});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I ended up doing this:
var options = [];
var parts = key.split('_');
var string = parts[0];
_.each(parts, function(p, i){
if (i > 0) string += '_' + p;
options.push(string);
});
var toUpdate = _.filter(subscribers, function(sub){
if (sub.var.indexOf(key + '_') === 0) return true;
if (options.indexOf(sub.var) > -1) return true;
return false;
});
So checking with indexOf on the string to see if there are children. And building an array with parents so any layer is a match, and doing an indexOf on that as well. I think this is the least complicated method of implementing it

RxJS pattern for initializing a toggle between two values

I am trying to figure out a nice RxJS pattern for a toggle-style button/link that basically limits a view of an array.
It toggles between two values, one of which is static, the other comes from an Observable (is result of an http request actually, this.itemsArray$).
ts
this.toggleClicks$ = Observable.fromEvent(this.viewAllToggle.nativeElement, 'click')
.scan(prev => !prev, false);
this.itemsArray$ = Observable.of([ /* array of any */ ]);
this.viewLimit$ = this.itemsArray$
.withLatestFrom(this.toggleClicks$, (items, viewAll) => {
return viewAll
? { nowLimit: items.length, otherLimit: 5 }
: { nowLimit: 5, otherLimit: items.length };
})
.share();
html
Showing <span>{{(viewLimit$ | async)?.nowLimit}}</span>.
<span #viewAllToggle>Show {{(viewLimit$ | async)?.otherLimit}}</span>
It basically works, but the obvious problem is the initial value, which is not populated until the first click. Even if I add .startWith()
this.itemsArray$ = Observable.of([ /* array of any */ ]);
it does not help.
What am I missing?
I did not get what you said about startWith. My understanding is that this should work :
this.toggleClicks$ = Observable.fromEvent(this.viewAllToggle.nativeElement, 'click')
.scan(prev => !prev, false)
.startWith (false); // OR TRUE, dont know what behaviour you seek
this.itemsArray$ = Observable.of([ /* array of any */ ]);
this.viewLimit$ = this.itemsArray$
.withLatestFrom(this.toggleClicks$, (items, viewAll) => {
return viewAll
? { nowLimit: items.length, otherLimit: 5 }
: { nowLimit: 5, otherLimit: items.length };
})
.share();
Is that what you tried?
The tricky thing here (and pretty much undocumented) with the withLatestFrom(obs$) operator is that nothing will be emitted till obs$ emits its first value. That is a logical behaviour as there is no value to be passed to the selector function (here in viewAll), and passing undefined by default might not necessarily be a sensible choice. If you know about the distinction between events and behaviours, the rationale behind this is that withLatestFrom is to be used with behaviours, which always have a value at any point of time.

Categories