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.
Related
I have a snippet of code here where i have an array that may or may not have keys in it. When the user presses on a 'friend' they add them to a list (array) where they might start a chat with them (add 3 friends to the array, then start a chatroom). The users selected might be toggled on or off.
Current Behavior:
i can add/remove one person, but i cant add multiple people to the array at the same time. When i add one person, select another - the first person is 'active', when i remove the first person, the second person automatically becomes active
Expected Behavior:
I would like to be able to add multiple people to the array and then remove any of the selected items from the array
onFriendChatPress = (key) => {
console.log(key) // this is my key 'JFOFK7483JFNRW'
let friendsChat = this.state.friendsChat // this is an empty array initially []
if (friendsChat.length === 0) {
friendsChat.push(key)
} else {
// there are friends/keys in the array loop through all possible items in the array to determine if the key matches any of the keys
for (let i = 0; i < this.state.selGame.friends.length; i++) {
// if the key matches, 'toggle' them out of the array
if (friendsChat[i] === key) {
friendsChat = friendsChat.filter(function (a) { return a !== key })
}
else {
return friendsChat.indexOf(key) === -1 ? friendsChat.push(key) :
}
}
}
}
Help please!
From your code, I was quite confused regarding the difference between this.state.selGame.friends and this.state.friendsChat. Maybe I missed something in your explication. However, I felt that your code seemed a bit too overcomplicated for something relatively simple. Here's my take on that task:
class Game {
state = {
friendsChat: [] as string[],
};
onFriendToggle(key: string) {
const gameRoomMembers = this.state.friendsChat;
if (gameRoomMembers.includes(key)) {
this.state.friendsChat = gameRoomMembers.filter(
(member) => member !== key
);
} else {
this.state.friendsChat = [...gameRoomMembers, key];
}
}
}
I used typescript because it makes things easier to see, but your JS code should probably give you a nice type inference as well. I went for readability over performance, but you can easily optimize the script above once you understand the process.
You should be able to go from what I sent you and tweak it to be according to what you need
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
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.
This may not be the completely ideal optimized way of accomplishing the task. I'm open for suggestions on any better ways. So far the loads work fine performancewise.
I have my knockout app working via ajax load. Inside the binding calls, I have a nested loop that includes a function that updates points based on a setting value.
When I attempt to add a new item, no errors are thrown, however the UI does not update and I can't seem to figure out why.
Here's a fiddle of what I'm trying to do.
http://jsfiddle.net/hjchvawr/2/
The addCombatant method does work, but for whatever reason the table will not rebind. You can see the added value in the VM json outputed to the console.
self.addCombatant = function(combatant){
ko.utils.arrayForEach(self.divisions(), function(d){
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
console.log(ko.toJSON(self.divisions));
}
)}.bind(this);
EDIT:
I've applied some updates suggested below and added another list to sort. It binds and updates however, when I add a combatant, it only binds to one event and the sorting is off. If I can't use sortDivision(combatants, 'swords'), how do would I make the automatic sorting work? In this fiddle (http://jsfiddle.net/4Lhwerst/2/) I want the event sorted by kills, then time. Is it possible to get this multilevel sorting done client side without creating another observeableArray?
This is the foreach binding in your table.
<!-- ko foreach: $root.sortDivision(combatants, 'swords') -->
sortDivision is defined:
self.sortDivision = function (div, evt) {
return div.sortBy(evt, 'time', 'asc').sortBy(evt, 'kills', 'desc');
};
Your sortBy function creates a new observableArray. That is not the same observableArray as is being pushed to.
ko.observableArray.fn.sortBy = function (evt, fld, direction) {
var isdesc = direction && direction.toLowerCase() == 'desc';
return ko.observableArray(this.sort(function (a, b) {
a = ko.unwrap(evt ? a[evt][fld]() : a[fld]());
b = ko.unwrap(evt ? b[evt][fld]() : b[fld]());
return (a == b ? 0 : a < b ? -1 : 1) * (isdesc ? -1 : 1);
}));
};
You should use computeds (or pureComputeds) for things that are a re-presentation or re-combination of data. Store any data item in one place.
You are pushing into the underlying combatants array and thus bypassing change tracking. Either remove the parentheses (d.combatants.push) or call valueHasMutated after you are done.
You need either:
if(d.name() == combatant.division){
d.combatants.push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
Or:
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
d.combatants.valueHasMutated();
}
I actually sure that there is a simple answer to that , and that I probably just don't use promises correctly but still.
Suppose I have a method that returns a promise and I have a method that needs to receive two parameters to work , one that I actually have already.
Now since I don't want my functions to be endless I split them to smaller ones , and now I just cant get hold of my parameter.
So to be clear , here is something that has not problem working ( might be a bit schematic though)
var origFeatures = [];
_.each(wrappedFeatures, function (wrappedFeature) {
bufferTasks.push(geometryEngineAsync.buffer(
[feature.geometry]
, feature.geometry.type === "polygon" ? [0] : [5]
, esriSRUnit_Meter));
}
}.bind(this));
all(bufferTasks).then(function(bufferedFeatures){
//doing something with the bufferedFeatures AND origFeatures
}.bind(this))
now Imaging the methods are long so when I split to this
defined somewhere before
function _saveGeometries(bufferedFeatures){
//do something with buffered features AND the original features
}
var origFeatures= [];
_.each(wrappedFeatures, function (wrappedFeature) {
bufferTasks.push(geometryEngineAsync.buffer(
[feature.geometry]
, feature.geometry.type === "polygon" ? [0] : [5]
, esriSRUnit_Meter));
}
}.bind(this));
all(bufferTasks)
.then(this._saveNewGeometries.bind(this))
I of course cannot see the original features because they are not in the scope of the wrapping function
So what I did try to do already is to create a a function in the middle and resolve it with the async result and the additional parameter, but the additional parameter is undefined
var featuresFromAttachLayer = [];
_.each(wrappedFeatures, function (wrappedFeature) {
bufferTasks.push(geometryEngineAsync.buffer(
[feature.geometry]
, feature.geometry.type === "polygon" ? [0] : [5]
, esriSRUnit_Meter));
}
}.bind(this));
all(bufferTasks).then(function(bufferedFeatures) {
var deff = new Deferred();
deff.resolve(bufferedFeatures, wrappedFeatures);
return deff.promise;
})
.then(this._saveNewGeometries.bind(this))
Any ideas how to pass the parameter the correct way , both on the good code side and the technical side.