I want to initialize 12 users in my list ${this.url}/users?offset=${offset}&limit=12 but with scrolling this offset should increase by 8 users.
I want to use infinite scrolling for that. My problem is that I'm using observables(userList) and I don't know how to append the new list of 8 members to the old one. In the tutorials in the internet the all use concat() but this is for arrays:/ I myself tried something to just call the whole list + 8 offset when loadMore is true but that somehow doesn't work.
My Code:
service.ts
// get a list of users
getList(offset= 0): Observable<any> {
return this.http.get(`${this.url}/users?offset=${offset}&limit=12`);
}
page.ts
#ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;
userList: Observable<any>;
offset = 0;
...
getAllUsers(loadMore = false, event?) {
if (loadMore) {
this.userList = this.userService.getList(this.offset += 8) //new 8 users
.pipe(map(response => response.results));
}
this.userList = this.userService.getList(this.offset) // initials 12 users
.pipe(map(response => response.results));
if (event) {
event.target.complete();
console.log(event);
console.log(loadMore);
}
}
page.html
...
</ion-item>
</ion-list>
<ion-infinite-scroll threshold="100px" (ionInfinite)="getAllUsers(true, $event)">
<ion-infinite-scroll-content
loadingSpinner="crescing"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-slide>
<ion-slide>
use Merge to merge multiple observables into a single observable:
getAllUsers(loadMore = false, event?) {
if (loadMore) {
const newUserList$ = this.userService.getList(this.offset += 8) //new 8 users
.pipe(map(response => response.results));
this.userList = merge(this.userList, newUserList$); // merge observables
}
this.userList = this.userService.getList(this.offset) // initials 12 users
.pipe(map(response => response.results));
if (event) {
event.target.complete();
console.log(event);
console.log(loadMore);
}
}
Update
From your URL maybe you should remove the limit parameter :
getList(offset= 0): Observable<any> {
return this.http.get(`${this.url}/users?offset=${offset}`);
}
As mentioned in other answers, this is a good use-case for the scan operator.
However, we must find a way to keep adding(accumulating) data when the user scrolls. I think this can be achieved by using a BehaviorSubject that will emit values on each scroll.
I opted for this type of subject because you will want to provide an initial value as well.
const loadUsersSubject = new BehaviorSubject<number>(12);
let userList$/* : Observable<any>; */ // Uncomment this if used inside the template along with the async pipe
let internalCnt = 0;
const generateUsers = (n: number) => {
return of(
Array.from({ length: n }, ((_, i) => ({ user: `user${++internalCnt}` })))
);
}
userList$ = loadUsersSubject
.pipe(
flatMap(numOfUsers => generateUsers(numOfUsers)),
scan((acc, crt) => [...acc, ...crt])
)
.subscribe(console.log)
// Scrolling after 1s..
timer(1000)
.subscribe(() => {
loadUsersSubject.next(8);
});
// Scrolling after 3s..
timer(3000)
.subscribe(() => {
loadUsersSubject.next(8);
});
StackBlitz
Here is how scan operator can be used to have a state that is augmented by following requests
https://stackblitz.com/edit/rxjs-h91d9u?devtoolsheight=60
import { of, Observable } from 'rxjs';
import { map, scan } from 'rxjs/operators';
const source = new Observable((observer) => {
observer.next(['Hello', 'World']);
setTimeout(() => {
observer.next(['will', 'concatenate']);
}, 1000)
setTimeout(() => {
observer.next(['also', 'will', 'concatenate']);
}, 2000)
}).pipe(
scan(
(acc, val) => acc.concat(val),
[]
)
);
source.subscribe(x => console.log(x));
Related
My FlatList does not update when the props I pass from redux change. Every time I send a message I increase everyones unread message count in both firebase and in my redux store. I made sure to include key extractor and extra data, but neither helps. The only thing that changes the unread message count is a reload of the device. How do I make sure the flatList updates with MapStateToProps. I made sure to create a new object by using Object.Assign:
action:
export const sendMessage = (
message,
currentChannel,
channelType,
messageType
) => {
return dispatch => {
dispatch(chatMessageLoading());
const currentUserID = firebaseService.auth().currentUser.uid;
let createdAt = firebase.database.ServerValue.TIMESTAMP;
let chatMessage = {
text: message,
createdAt: createdAt,
userId: currentUserID,
messageType: messageType
};
FIREBASE_REF_MESSAGES.child(channelType)
.child(currentChannel)
.push(chatMessage, error => {
if (error) {
dispatch(chatMessageError(error.message));
} else {
dispatch(chatMessageSuccess());
}
});
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child(channelType)
.child(currentChannel).child('users')
UNREAD_MESSAGES.once("value")
.then(snapshot => {
snapshot.forEach(user => {
let userKey = user.key;
// update unread messages count
if (userKey !== currentUserID) {
UNREAD_MESSAGES.child(userKey).transaction(function (unreadMessages) {
if (unreadMessages === null) {
dispatch(unreadMessageCount(currentChannel, 1))
return 1;
} else {
alert(unreadMessages)
dispatch(unreadMessageCount(currentChannel, unreadMessages + 1))
return unreadMessages + 1;
}
});
} else {
UNREAD_MESSAGES.child(userKey).transaction(function () {
dispatch(unreadMessageCount(currentChannel, 0))
return 0;
});
}
}
)
})
};
};
export const getUserPublicChannels = () => {
return (dispatch, state) => {
dispatch(loadPublicChannels());
let currentUserID = firebaseService.auth().currentUser.uid;
// get all mountains within distance specified
let mountainsInRange = state().session.mountainsInRange;
// get the user selected mountain
let selectedMountain = state().session.selectedMountain;
// see if the selected mountain is in range to add on additional channels
let currentMountain;
mountainsInRange
? (currentMountain =
mountainsInRange.filter(mountain => mountain.id === selectedMountain)
.length === 1
? true
: false)
: (currentMountain = false);
// mountain public channels (don't need to be within distance)
let currentMountainPublicChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Public");
// mountain private channels- only can see if within range
let currentMountainPrivateChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Private");
// get public channels
return currentMountainPublicChannelsRef
.orderByChild("key")
.once("value")
.then(snapshot => {
let publicChannelsToDownload = [];
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
// add the channel ID to the download list
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
// flag whether you can check in or not
if (currentMountain) {
dispatch(checkInAvailable());
} else {
dispatch(checkInNotAvailable());
}
// if mountain exists then get private channels/ if in range
if (currentMountain) {
currentMountainPrivateChannelsRef
.orderByChild("key")
.on("value", snapshot => {
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",
snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
});
}
return publicChannelsToDownload;
})
.then(data => {
setTimeout(function () {
dispatch(loadPublicChannelsSuccess(data));
}, 150);
});
};
};
Reducer:
case types.UNREAD_MESSAGE_SUCCESS:
const um = Object.assign(state.unreadMessages, {[action.info]: action.unreadMessages});
return {
...state,
unreadMessages: um
};
Container- inside I hook up map state to props with the unread messages and pass to my component as props:
const mapStateToProps = state => {
return {
publicChannels: state.chat.publicChannels,
unreadMessages: state.chat.unreadMessages,
};
}
Component:
render() {
// rendering all public channels
const renderPublicChannels = ({ item, unreadMessages }) => {
return (
<ListItem
title={item.info.Name}
titleStyle={styles.title}
rightTitle={(this.props.unreadMessages || {} )[item.id] > 0 && `${(this.props.unreadMessages || {} )[item.id]}`}
rightTitleStyle={styles.rightTitle}
rightSubtitleStyle={styles.rightSubtitle}
rightSubtitle={(this.props.unreadMessages || {} )[item.id] > 0 && "unread"}
chevron={true}
bottomDivider={true}
id={item.Name}
containerStyle={styles.listItemStyle}
/>
);
};
return (
<View style={styles.channelList}>
<FlatList
data={this.props.publicChannels}
renderItem={renderPublicChannels}
keyExtractor={(item, index) => index.toString()}
extraData={[this.props.publicChannels, this.props.unreadMessages]}
removeClippedSubviews={false}
/>
</View>
);
}
}
Object.assign will merge everything into the first object provided as an argument, and return the same object. In redux, you need to create a new object reference, otherwise change is not guaranteed to be be picked up. Use this
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
// or
const um = {...state.unreadMessages, [action.info]: action.unreadMessages }
Object.assign() does not return a new object. Due to which in the reducer unreadMessages is pointing to the same object and the component is not getting rerendered.
Use this in your reducer
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
I've got following Epic which works well in application, but I can't get my marble test working. I am calling action creator in map and it does return correct object into stream, but in the test I am getting empty stream back.
export const updateRemoteFieldEpic = action$ =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
const { orderId, fields } = payload;
const requiredFieldIds = [4, 12]; // 4 = Name, 12 = Client-lookup
const requestData = {
id: orderId,
customFields: fields
.map(field => {
return (!field.value && !requiredFieldIds.includes(field.id)) ||
field.value
? field
: null;
})
.filter(Boolean)
};
if (requestData.customFields.length > 0) {
return from(axios.post(`/customfields/${orderId}`, requestData)).pipe(
map(() => queueAlert("Draft Saved")),
catchError(err => {
const errorMessage =
err.response &&
err.response.data &&
err.response.data.validationResult
? err.response.data.validationResult[0]
: undefined;
return of(queueAlert(errorMessage));
})
);
}
return of();
})
);
On successfull response from server I am calling queueAlert action creator.
export const queueAlert = (
message,
position = {
vertical: "bottom",
horizontal: "center"
}
) => ({
type: QUEUE_ALERT,
payload: {
key: uniqueId(),
open: true,
message,
position
}
});
and here is my test case
describe("updateRemoteFieldEpic", () => {
const sandbox = sinon.createSandbox();
let scheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
afterEach(() => {
sandbox.restore();
});
it("should return success message", () => {
scheduler.run(ts => {
const inputM = "--a--";
const outputM = "--b--";
const values = {
a: updateRemoteField({
orderId: 1,
fields: [{ value: "test string", id: 20 }],
update: true
}),
b: queueAlert("Draft Saved")
};
const source = ActionsObservable.from(ts.cold(inputM, values));
const actual = updateRemoteFieldEpic(source);
const axiosStub = sandbox
.stub(axios, "post")
.returns([]);
ts.expectObservable(actual).toBe(outputM, values);
ts.flush();
expect(axiosStub.called).toBe(true);
});
});
});
output stream in actual returns empty array
I tried to return from map observable of the action creator which crashed application because action expected object.
By stubbing axios.post(...) as [], you get from([]) in the epic - an empty observable that doesn't emit any values. That's why your mergeMap is never called. You can fix this by using a single-element array as stubbed value instead, e.g. [null] or [{}].
The below is an answer to a previous version of the question. I kept it for reference, and because I think the content is useful for those who attempt to mock promise-returning functions in epic tests.
I think your problem is the from(axios.post(...)) in your epic. Axios returns a promise, and the RxJS TestScheduler has no way of making that synchronous, so expectObservable will not work as intended.
The way I usually address this is to create a simple wrapper module that does Promise-to-Observable conversion. In your case, it could look like this:
// api.js
import axios from 'axios';
import { map } from 'rxjs/operators';
export function post(path, data) {
return from(axios.post(path, options));
}
Once you have this wrapper, you can mock the function to return a constant Observable, taking promises completely out of the picture. If you do this with Jest, you can mock the module directly:
import * as api from '../api.js';
jest.mock('../api.js');
// In the test:
api.post.mockReturnValue(of(/* the response */));
Otherwise, you can also use redux-observable's dependency injection mechanism to inject the API module. Your epic would then receive it as third argument:
export const updateRemoteFieldEpic = (action$, state, { api }) =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
// ...
return api.post(...).pipe(...);
})
);
In your test, you would then just passed a mocked api object.
I'm trying to create a list of events that a user is going to. First I get event keys and then what I would like to do is subscribe to each event and listen for changes. Currently only the last event works because this.eventRef is being changed in the for loop.
eventRef: AngularFireObject<any>
getEvents() {
const eventsGuestsLookup = this.db.object(`eventsGuestsLookup/${this.uid}`).valueChanges()
this.eventsGuestsLookupSub = eventsGuestsLookup
.subscribe(eventKeys => {
if (eventKeys) {
console.log(eventKeys)
for (const k in eventKeys) {
if (eventKey.hasOwnProperty(k)) {
this.eventRef = this.db.object(`events/${k}`)
console.log(this.eventRef)
this.eventRef.snapshotChanges().subscribe(action => {
const key = action.payload.key
const event = { key, ...action.payload.val() }
this.makeEvents(event)
})
}
}
}
})
}
What I do next is get the user's response and for each status I want to display certain information. I don't know any other way of doing this, so I check both lists attending and notAttending and if there is a response from the user I change the event properties.
makeEvents(event) {
console.log(event)
event.goingText = "RSVP"
event.setGoing = 'rsvp'
event.setColor = "rsvp-color"
const attending = this.db.object(`attendingLookup/${this.uid}/${event.key}`).valueChanges()
this.attendingLookupSub = attending
.subscribe(data => {
console.log('attending', data)
if (data) {
event.goingText = "ATTENDING"
event.setGoing = 'thumbs-up'
event.setColor = 'attending-color'
}
})
const notAttending = this.db.object(`not_attendingLookup/${this.uid}/${event.key}`).valueChanges()
this.notAttendingLookupSub = notAttending
.subscribe(data => {
console.log('not attending', data)
if (data) {
event.goingText = "NOT ATTENDING"
event.setGoing = 'thumbs-down'
event.setColor = 'not-attending-color'
}
})
this.events.push(event)
}
*** Edit
const eventsGuestsLookup = this.db.object(`eventsGuestsLookup/${this.uid}`).valueChanges()
eventsGuestsLookup.subscribe(keys => {
of(keys).pipe(
mergeMap(keys => {
Object.keys(keys).map(k => {
console.log(k)
})
return merge(Object.keys(keys).map(k => this.db.object(`events/${k}`)))
})
).subscribe(data => console.log('data', data))
})
what you want to acheive is flat your observables collection. to acheive it you can do something like this :
//Dummy eventKeys observable.
const obs1$ = new BehaviorSubject({key:1, action: 'lorem'});
const obs2$ = new BehaviorSubject({key:2, action: 'lorem'});
const obs3$ = new BehaviorSubject({key:3, action: 'lorem'});
const eventKeys = {
obs1$,
obs2$,
obs3$
};
// Dummy eventsGuestsLookup observable.
of(eventKeys)
.pipe(
//eventsGuestsLookup dispatch collection of obserbable, we want to flat it.
mergeMap(ev => {
// We merge all observables in new one.
return merge(...Object.keys(ev).map(k => ev[k]));
}),
).subscribe(console.log);
inportant note : ev[k] is an Observable object. On your case you should do something like :
.map(k => this.db.object(`events/${k}`)) // will return observable.
live demo
I have the following piece of code. As is, with a couple of lines commented out, it works as expected. I subscribe to a stream, do some processing and stream the data to the client. However, if I uncomment the comments, my stream is always empty, i.e. count in getEntryQueryStream is always 0. I suspect it has to do with the fact that I subscribe late to the stream and thus miss all the values.
// a wrapper of the mongodb driver => returns rxjs streams
import * as imongo from 'imongo';
import * as Rx from 'rx';
import * as _ from 'lodash';
import {elasticClient} from '../helpers/elasticClient';
const {ObjectId} = imongo;
function searchElastic({query, sort}, limit) {
const body = {
size: 1,
query,
_source: { excludes: ['logbookType', 'editable', 'availabilityTag'] },
sort
};
// keep the search results "scrollable" for 30 secs
const scroll = '30s';
let count = 0;
return Rx.Observable
.fromPromise(elasticClient.search({ index: 'data', body, scroll }))
.concatMap(({_scroll_id, hits: {hits}}) => {
const subject = new Rx.Subject();
// subject needs to be subscribed to before adding new values
// and therefore completing the stream => execute in next tick
setImmediate(() => {
if(hits.length) {
// initial data
subject.onNext(hits[0]._source);
// code that breaks
//if(limit && ++count === limit) {
//subject.onCompleted();
//return;
//}
const handleDoc = (err, res) => {
if(err) {
subject.onError(err);
return;
}
const {_scroll_id, hits: {hits}} = res;
if(!hits.length) {
subject.onCompleted();
} else {
subject.onNext(hits[0]._source);
// code that breaks
//if(limit && ++count === limit) {
//subject.onCompleted();
//return;
//}
setImmediate(() =>
elasticClient.scroll({scroll, scrollId: _scroll_id},
handleDoc));
}
};
setImmediate(() =>
elasticClient.scroll({scroll, scrollId: _scroll_id},
handleDoc));
} else {
subject.onCompleted();
}
});
return subject.asObservable();
});
}
function getElasticQuery(searchString, filter) {
const query = _.cloneDeep(filter);
query.query.filtered.filter.bool.must.push({
query: {
query_string: {
query: searchString
}
}
});
return _.extend({}, query);
}
function fetchAncestors(ancestorIds, ancestors, format) {
return imongo.find('session', 'sparse_data', {
query: { _id: { $in: ancestorIds.map(x => ObjectId(x)) } },
fields: { name: 1, type: 1 }
})
.map(entry => {
entry.id = entry._id.toString();
delete entry._id;
return entry;
})
// we don't care about the results
// but have to wait for stream to finish
.defaultIfEmpty()
.last();
}
function getEntryQueryStream(entriesQuery, query, limit) {
const {parentSearchFilter, filter, format} = query;
return searchElastic(entriesQuery, limit)
.concatMap(entry => {
const ancestors = entry.ancestors || [];
// if no parents => doesn't match
if(!ancestors.length) {
return Rx.Observable.empty();
}
const parentsQuery = getElasticQuery(parentSearchFilter, filter);
parentsQuery.query.filtered.filter.bool.must.push({
terms: {
id: ancestors
}
});
// fetch parent entries
return searchElastic(parentsQuery)
.count()
.concatMap(count => {
// no parents match query
if(!count) {
return Rx.Observable.empty();
}
// fetch all other ancestors that weren't part of the query results
// and are still a string (id)
const restAncestorsToFetch = ancestors.filter(x => _.isString(x));
return fetchAncestors(restAncestorsToFetch, ancestors, format)
.concatMap(() => Rx.Observable.just(entry));
});
});
}
function executeQuery(query, res) {
try {
const stream = getEntryQueryStream(query);
// stream is passed on to another function here where we subscribe to it like:
// stream
// .map(x => whatever(x))
// .subscribe(
// x => res.write(x),
// err => console.error(err),
// () => res.end());
} catch(e) {
logger.error(e);
res.status(500).json(e);
}
}
I don't understand why those few lines of code break everything or how I could fix it.
Your use case is quite complex, you can start off with building up searchElastic method like the pattern bellow.
convert elasticClient.scroll to an observable first
setup the init data for elasticClient..search()
when search is resolved then you should get your scrollid
expand() operator let you recursively execute elasticClientScroll observable
use map to select data you want to return
takeWhile to decide when to complete this stream
The correct result will be once you do searchElastic().subscribe() the stream will emit continuously until there's no more data to fetch.
Hope this structure is correct and can get you started.
function searchElastic({ query, sort }, limit) {
const elasticClientScroll = Observable.fromCallback(elasticClient.scroll)
let obj = {
body: {
size: 1,
query,
_source: { excludes: ['logbookType', 'editable', 'availabilityTag'] },
sort
},
scroll: '30s'
}
return Observable.fromPromise(elasticClient.search({ index: 'data', obj.body, obj.scroll }))
.expand(({ _scroll_id, hits: { hits } }) => {
// guess there are more logic here .....
// to update the scroll id or something
return elasticClientScroll({ scroll: obj.scroll, scrollId: _scroll_id }).map(()=>
//.. select the res you want to return
)
}).takeWhile(res => res.hits.length)
}
The below observable creates an array of event objects.
eventsRef: AngularFireList<any>;
events: Observable<any>;
this.eventsRef = db.list('events');
this.events = this.eventsRef.snapshotChanges().map(changes => {
return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
});
I need to add additional data to this.events from other database lists. So I need each event object to contain a guest count and data eventsFilters. I'm not sure how to do that. This is what I have so far:
this.events = this.eventsRef.snapshotChanges().map(changes => {
changes.map(data => {
console.log(data.payload.key)
this.db.object(`/eventsFilters/${data.payload.key}`)
.valueChanges()
.subscribe(data => {
console.log(data) //event filters
})
})
changes.map(data => {
console.log(data.payload.key)
this.db.object(`/eventsGuests/${data.payload.key}`)
.valueChanges()
.subscribe(data => {
let guestCount = Object.keys(data).length;
console.log(guestCount)
this.guestCount = guestCount; //guest count
})
})
return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
});
Edit --------
I got this far using combineLatest but I'm still not sure how to group each event data.
this.eventsRef.snapshotChanges()
.switchMap(
(changes) => {
let userQueries: Observable<any>[] = [];
let lists: Array<string> = ['eventsFilters', 'eventsGuests'];
changes.map(data => {
for (let list of lists) {
userQueries.push(this.db.object(`/${list}/${data.payload.key}`).valueChanges());
}
})
userQueries.push(this.eventsRef.snapshotChanges());
return Observable.combineLatest(userQueries);
})
.subscribe((d) => {
console.log(d)
});
console.log(d) outputs something like this:
[
{}, //object with data from eventsFilters for first event
{}, //object with data from eventsGuests for first event
{}, //object with data from eventsFilters for second event
{}, //object with data from eventsGuests for second event
...
[{},{} ...] //array with all events
]
Here is an example combining 3 observable:
combinedData$ = combineLatest( entityList$, settings$, currentUser$).pipe(
map(([entityList, pageSetting, currentUser]) => {
//entityList, pageSettingand currentUser holds the last value emitted on each observables.
if (!pageSetting.ShowAllOrganisation) {
//If not showing all organisation, then we have to filter it
retVal = entityList.filter(entity=> entity.organisationId === currentUser.organisationId);
}
return retVal;
})
);
CombineLatest will return a new observable. If one of the 3 observable emmits a new value, the combineLatest will be triggered and emmits a new value. For more info on how combineLatest is working visit the official documentation