Suppose I have a collection with field foo, and I'd like to get the count of unique values.
Collection.distinct('foo').length;
I would like to have the value available in my template, such as {{ fooCount }}.
How could I do this, and have the number be reactive in Meteor?
It would be something like this:
Template.yourTemplateName.helpers({
fooCount() {
var foos = CollectionName.find({foo: {$exists: true}})
.map(d => d.foo);
return _.uniq(foos).length;
}
});
this will be reactive because it is a template helper using a reactive data source. Note the use of underscores with is part of core meteor.
also, just for fun you could do this:
Template.yourTemplateName.helpers({
fooCount() {
return CollectionName.find({foo: {$exists: true}})
.map(d => d.foo)
.reduce((acc, b) => _.contains(acc, b) ?
acc : acc.concat(b), []).length
}
});
Related
I need to lookup the location for each product.
First searchCollection returns array of object with one of the property anotherTblDocIs as a json string.
In this json string, which is firebase docId, I need to lookup the name. Then I need to merge it back.
How can I achieve this properly?
this.firebaseService
.searchCollection('product', 'shopId', this.shopId)
.pipe(
mergeMap((products: any) => {
let observableBatch = [];
products.map(product => {
const docIds = JSON.parse(product['anotherTblDocIs']);
docIds.forEach(doc =>{
observableBatch.push(this.firebaseService.readCollectionByDocId('doc', doc ).pipe(map(u => {return { doc, ...u}})))
});
});
return forkJoin(observableBatch);
})
)
.subscribe(products => {
});
When you write a stream it's often better to break things into small steps, and you keep transforming the stream until what you need.
Maybe this won't be exactly equivalent in terms of orders of subscriptions, but I think it would look better with something like:
this.firebaseService.searchCollection('product', 'shopId', this.shopId).pipe(
// Here we have Observable<Product[]>
mergeMap(products => products)
// Now we have Observable<Product>
mergeMap(product => JSON.parse(product['anotherTblDocIds']))
// Now we have Observable<DocIds> (for each product)
mergeMap(docId => this.firebaseService.readCollectionByDocId('doc', doc).pipe(
// We need to this map inside this stream to have the reference to `docId`
map(u => ({ doc: docId, ...u }))
))
)
mergeMap will flatten both Observables and arrays, so it can be used this way.
Following this question: Add data to http response using rxjs
I've tried to adapt this code to my use case where the result of the first http call yields an array instead of a value... but I can't get my head around it.
How do I write in rxjs (Typescript) the following pseudo code?
call my server
obtain an array of objects with the following properties: (external id, name)
for each object, call another server passing the external id
for each response from the external server, obtain another object and merge some of its properties into the object from my server with the same id
finally, subscribe and obtain an array of augmented objects with the following structure: (external id, name, augmented prop1, augmented prop2, ...)
So far the only thing I was able to do is:
this._appService
.getUserGames()
.subscribe(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
_.forEach(this._userGames, game => {
this._externalService
.getExternalGameById(game.externalGameId)
.subscribe(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
});
});
});
Thanks in advance
I found a way to make it work. I'll comment the code to better explain what it does, especially to myself :D
this._appService
.getUserGames() // Here we have an observable that emits only 1 value: an any[]
.mergeMap(games => _.map(games, game => this._externalService.augmentGame(game))) // Here we map the any[] to an Observable<any>[]. The external service takes the object and enriches it with more properties
.concatAll() // This will take n observables (the Observable<any>[]) and return an Observable<any> that emits n values
.toArray() // But I want a single emission of an any[], so I turn that n emissions to a single emission of an array
.subscribe(games => { ... }); // TA-DAAAAA!
Don't use subscribe. Use map instead.
Can't test it, but should look more like this:
this._appService
.getUserGames()
.map(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
return this._userGames.map(game => { /* this should return an array of observables.. */
return this._externalService
.getExternalGameById(game.externalGameId)
.map(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
return game;
});
});
})
.mergeAll()
.subscribe(xx => ...); // here you can subscribe..
I am converting Mongo.Cursor to array using fetch() in Tracker.autorun and assigning it to the songsArray. But each time the underlying database is changed(reactively), I see duplicate values in songsArray
private songsArray:Array<any>;
songsCursor:Mongo.Cursor<any>;
//...Some code here
ngOnInit():any {
//... Some code here
this.songsCursor = Songs.find();
Tracker.autorun(() => {
this.songsArray = [];
this.songsArray = this.songsCursor.fetch();
});
}
Why is it happening and if I assume I am doing it wrong, then what is the correct way to convert cursors to array in Tracker.autorun?
In your constructor you need to do something like this:
$reactive(this).attach($scope);
// Subscribe to collections here:
this.subscribe('songs');
this.helpers({
songs: () => Songs.find()
});
and you'll find songs is an array like you want
I am beginner in programming and interested to learn MeteorJS. I want to search category_name and subcategory_name by keyword or alphabet.:)
This is my code.
collections: subcategory
{
_id:"ZbwCsJEMi2DesyJA7",
category_name: "ICT",
subcategory_name: "Laptop"
}
subcategory.js
Template.Subcategory.events({
"keyup .searchbox": function(event){
var query = event.target.value;
Session.set('query', query);
}
});
Template.Subcategory.helpers({
subcategory: function(){
var filter = {sort: {}};
var query = Session.get('query');
filter.sort[Session.get('sortby')] = 1;
return Subcategory.find({ subcategory_name: new RegExp(query, 'i')} , filter );
}
});
Seems like you are looking for $or operator:
var queryRegexp = new RegExp(query, 'i');
return Subcategory.find({
$or: [
{ category_name: queryRegexp },
{ subcategory_name: queryRegexp }
]
} , filter);
Official Mongo's documentation for $or: https://docs.mongodb.org/manual/reference/operator/query/or/
You'll have to create a text index in Mongo. You can do this in Meteor still I think with something like this:
MyCollection._ensureIndex({
"$**": "text"
});
The above uses a wildcard but you can be more specific. See https://docs.mongodb.org/v3.0/core/index-text/ for more info.
For larger collections I tend to pass a text search query through a subscriptions so it can be performed on the server.
Assuming you want to show a list of things and allow users to filter them with the search box, you could do something like this:
Meteor.publish('MyCollection', function (searchTerm) {
return searchTerm ? MyCollection.find() : MyCollection.find({ $text: {$search: searchTerm} });
});
On the client, assuming you're using template-level subscriptions, you could set up your subscription like this:
Template.Subcategory.onCreated(function(){
var self = this;
// requires the reactive-var package
self.searchTerm = new ReactiveVar(false);
self.autorun(function(){
self.subscribe( "MyCollection", self.searchTerm.get() );
});
});
It'd then just be a case of setting your search term:
Template.Subcategory.events({
'keyup .searchbox': function(e,t){
var inputValue = e.currentTarget.value,
//you could set an arbitrary minimum search term length like so
searchTerm = inputValue.length > 1 ? inputValue : false;
t.searchTerm.set(searchTerm);
}
});
There's a couple of caveats on relying on the subscriptions so heavily like this. For example, if your collections are scoped globally on the client you run the risk of multiple subscriptions to the same collection giving you results you might not want to render within your list. Nevertheless, I quite like this approach. Food for thought.
I have a list of company names I'm populating from a collection
the helper function I have is:
Template.companyList.helpers({
companies: function () {
return Companies.find({owner: Meteor.userId()}, {sort: {a: 1}, name:1, createdAt:1});
}
});
It's looped through using a
{{#each companies}}
which outputs
<LI> Company Name </LI>
Above this I have a text box, and would like to filter the list of companies by what I type in the textbox - I'd prefer to have a "containing" filter as opposed to "starting with" filter, but i'll take either one - is there an established way of doing this in Meteor? If not, is there a plugin that someone wrote that does this?
Also, whatever answer you give, please consider the fact that I've been using Meteor for, oh, 5 days now, and i'm still learning it, so, a Newbie style answer would be great.
Thanks for Reading!
edit
This is the updated answer I came up with - combining David's answer with my previous companies helper:
Template.companyList.helpers({
companies: function () {
var query = Session.get('query');
selector = {owner: Meteor.userId()};
options = {sort: {a: 1}, companyName:1, createdAt:1};
if (query && query.length) {
var re = new RegExp(query, 'i');
selector.companyName = re;
}
return Companies.find(selector, options);
}
});
Here is the outline for a simple search-as-you-type interface:
Template.myTemplate.helpers({
companies: function() {
// build a regular expression based on the current search
var search = Session.get('search');
var re = new RegExp(search, 'i');
selector = {owner: Meteor.userId()};
// add a search filter only if we are searching
if (search && search.length)
selector.name = re;
options = {sort: {createdAt: -1}};
return Companies.find(selector, options);
}
});
Template.myTemplate.events({
'keyup #search': function() {
// save the current search query in a session variable as the user types
return Session.set('search', $('#search').val());
}
});
This assumes:
You are trying to search Companies by name.
You have an input with an id of search.
Please modify as needed for your use case. Let me know if you have any questions.