Firebase - populate property/array with additional data - javascript

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

Related

How to append one observable to another?

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));

How to filter an BehaviorSubject array

I want to filter on an array that is a BehaviorSubject type and update the value of one of the object properties of the array.
public users$: BehaviorSubject<IUser[]> = new BehaviorSubject<IUser[]>([]);
{
[key:'id',username:'John'],
[key:'id',username:'David'],
[key:'id',username:'Sara']
}
this.breadcrumbs$
.pipe(
map(user=> from(user)
.pipe(first(x => x.key === key)))
).subscribe(res => {
const index = this.usersValue.indexOf(res);
this.usersValue[index]['username'] = label;
});
I want to get it done with rxjs
Try this one.
this.breadcrumbs$.pipe(
map(users => users.find(user => user.key === key)))
.subscribe(res => {
const index = this.usersValue.indexOf(res);
this.usersValue[index]['username'] = label;
});

Angular | Subscribe to multiple observables

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

rxjs subscribing late results to empty stream

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)
}

dynamic loop base on object key javascript

I have a list, I have a search bar for filtering,
handleSearch = e => {
const q = e.target.value
if(q){
const filtered = this.state.data.filter(o => {
return o['name'].includes(q)
})
this.setState({
data: filtered
})
}else{
this.setState({
data: this.state.source
})
}
}
the problem with this method is that I hardcoded o['name'] which is a problem if my list has multiple property.
https://codesandbox.io/s/420lxz97r4
You can use Object.keys and Array.some:
return Object.keys(o).some(e => o[e].includes(q))

Categories

Resources