Let's say you want to dynamically insert into different collections. Right now I am using a switch statement:
switch (i) {
case "dog":
Dog.insert({
name: "Skippy"
});
break;
case "cat":
Cat.insert({
name: "Skippy"
});
break;
}
But this is messy, and if I need to support future collections, it fails. Is there a way to choose the collection based on "i" in the example above?
Correct me if I am wrong but I think this is what you are trying to do:
var Dog = {
insert: function(props) {
console.log(props);
}
}
var insertArbitraryDocument = (function(collectionType, props) {
window[collectionType].insert(props)
}).bind(this);
insertArbitraryDocument('Dog', {name: 'skippy'}); //=> {name: 'skippy'}
In this snippet you are accessing the window object and getting the property of whatever name you are passing in (must be exactly the same as the collection). Then you can call your usual function calls.
I don't think there is a meteor built-in way of doing this, but it's pretty easy to just create a directory of collections manually:
JS in common to client and server:
var collections = {};
function myColl(name) {
var coll = new Meteor.Collection(name);
collections[name] = coll;
return coll;
}
// and now just use myColl instead of new Meteor.Collection
Dog = myColl('dog');
And then, to do what you want to do:
collections[i].insert(data);
Here's a complete working example:
Posts = new Mongo.Collection('posts');
Comments = new Mongo.Collection('comments');
var capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
var nameToCollection = function(name) {
// pluralize and capitalize name, then find it on the global object
// 'post' -> global['Posts'] (server)
// 'post' -> window['Posts'] (client)
var root = Meteor.isClient ? window : global;
return root[capitalize(name) + 's'];
};
var insertSomething = function(name, data) {
var collection = nameToCollection(name);
collection.insert(data);
}
Meteor.startup(function() {
// ensure all old documents are removed
Posts.remove({});
Comments.remove({});
// insert some new documents
insertSomething('post', {message: 'this a post'});
insertSomething('comment', {message: 'this a comment'});
// check that it worked
console.log(Posts.findOne());
console.log(Comments.findOne());
});
Note this is nearly identical to this question but I simplified the answer for more generic use.
Related
I've got one view displaying some pictures published by users with some data (let's image Instagram).
I already have these pictures as non-reactive data (otherwise you could see many updates) but these images have one button to like the picture. If I have this as non-reactive data I can't see when I click on "Like" the filled heart (I need to refresh).
This is my subscribe function:
this.subscribe('food', () => [{
limit: parseInt(this.getReactively('perPage')),
//skip: parseInt((this.getReactively('page') - 1) * this.perPage),
sort: this.getReactively('sort')
}, this.getReactively('filters'), this.getReactively('searchText'), this.getReactively('user.following')
]);
And this is my helper:
food() {
const food = Food.find({}, {reactive: true}, {
sort: this.sort
}).fetch().map(food => {
const owner = Meteor.users.findOne(food.owner, {fields: {username: 1, avatarS: 1, following: 1}});
food.avatarS = owner && owner.avatarS;
food.username = owner && owner.username;
if (food.likes.indexOf(Meteor.userId()) == -1) {
// user did not like this plate
food.liked = false;
} else {
// user liked this plate
food.liked = true;
}
return food;
});
}
Is possible to have a non-reactive model but with some reactive properties on it?
I'm using Angular 1.X with TS btw
Thanks in advance!
PS: is it normal that this works as non-reactive when I change reactive to true?
Modification to your code:
//console.log(food.likes);
this.subscribe('reactiveFoodData', {ownerId: food.owner, userId: Meteor.userId()}).subscribe(()=>{
console.log(this.user);
});
// THIS IS THE PUBLISH METHOD LOCATED IN THE SERVER SIDE:
Meteor.publish('reactiveFoodData', function(params: {ownerId:string, userId:string) {
const owner = Meteor.users.findOne(params.ownerId);
if (!owner) {
throw new Meteor.Error('404', 'Owner does not exist');
}
let result = {};
result.avatarS = owner.avatarS;
result.username = owner.username;
const food = Food.find({});
result.liked = !(food.likes.indexOf(params.userId) == -1);
return result;
});
You have few problems:
1. The reactive flag is true by default, you do not need to set it.
2. The function find is accepting only two arguments, not 3.
Should be:
const food = Food.find({}, {reactive: true, sort: this.sort})
If you need some, subset of data to be reactive only (from some collection). You could create a specific Method (which udpates only "likes").
https://guide.meteor.com/methods.html
UPDATE:
Here is how you write a method with return parameter (check two examples, with Future and without):
How to invoke a function in Meteor.methods and return the value
UPDATE2:
You have lost reactivity when you used fetch(). Because you moved from reactive cursor to just simple array over which you map values. Do not expect reactivity after fetch(). If you want fetch or do not want to use Cursors, you could wrap the find inside Tracker.autorun(()=>{}) or utilize publish/subscribe.
Note: But be careful, if you somehow manage to get "empty" cursor in find(), your Tracker.autorun will stop react reactively. Autorun works only if it has something to watch over.
The main point with method, is that if you want to have one time non-reactive action for something. You define the method on server:
Meteor.methods({
myMethod: ()=> {
return "hello";
}
});
And you can call it from client with:
Meteor.call('myMethod', (error, result) => {
console.log(result); // "hello"
});
Instead of working with pure collections. You could start using publish/subscribe. On server you publish 'likes' and on client you just listens to this new reactive view. E.g.,
Meteor.publish('likes', (options: {owner: string, likes: Array<any>}) => {
let result: any = {}
const owner = Meteor.users.findOne(options.owner, username: 1, avatarS: 1, following: 1}});
result.avatarS = options.owner && options.owner.avatarS;
result.username = options.owner && options.owner.username;
result.liked = !(options.likes.indexOf(Meteor.userId()) == -1)
return result;
});
On client side: Meteor.subscibe('likes', {food.owner, food.likes}).subscribe(()=>{});
This is just off the top of my head.
Have you tried looking at Tracker ? https://docs.meteor.com/api/tracker.html
But more specifically the method Tracker.nonreactive
https://docs.meteor.com/api/tracker.html#Tracker-nonreactive
Is there a way (maybe using rules) to duplicate data on add/push to firebase?
What I want to archive is when I do an add to a firebase array I want to duplicate the data to another array.
So this is my firebase structure:
my-firebase: {
items: [ ... ],
queue: [ ... ]
}
And this is how I have my services defined:
.factory('Service1',['$firebaseArray', function($firebaseArray) {
var items = new Firebase('my-firebase.firebaseio.com/items');
return $firebaseArray(items);
}])
.factory('Service2',['$firebaseArray', function($firebaseArray) {
var queue = new Firebase('my-firebase.firebaseio.com/queue');
return $firebaseArray(queue);
}])
And here is how I use them:
.controller('controller', function($scope, Service1, Service2) {
$scope.save = function() {
Service1.$add({name: "test1"});
Service2.$add({name: "test1"});
}
};
And want I to have a single call not a duplicate call/code but having the result in both arrays (items and queue).
Thanks so much!
Always remember that AngularFire is a relatively thin wrapper around Firebase's JavaScript SDK that helps in binding data into your AngularJS views. If you're not trying to bind and something is not immediately obvious, you'll often find more/better information in the documentation of Firebase's JavaScript SDK.
The API documentation for $firebaseArray.$add() is helpful for this. From there:
var list = $firebaseArray(ref);
list.$add({ foo: "bar" }).then(function(ref) {
var id = ref.key();
console.log("added record with id " + id);
list.$indexFor(id); // returns location in the array
});
So $add() returns a promise that is fulfilled when the item has been added to Firebase. With that knowledge you can add a same-named child to the other list:
var queue = new Firebase('my-firebase.firebaseio.com/queue');
$scope.save = function() {
Service1.$add({name: "test1"}).then(function(ref) {
queue.child(ref.key().set({name: "test1"});
});
}
This last snippet uses a regular Firebase reference. Since AngularFire builds on top of the Firebase JavaScript SDK, they work perfectly together. In fact: unless you're binding these $firebaseArrays to the $scope, you're better off not using AngularFire for them:
var items = new Firebase('my-firebase.firebaseio.com/items');
var queue = new Firebase('my-firebase.firebaseio.com/queue');
$scope.save = function() {
var ref = queue.push();
ref.set({name: "test1"})
queue.child(ref.key().set({name: "test1"});
}
To my eyes this is much easier to read, because we're skipping a layer that wasn't being used. Even if somewhere else in your code, you're binding a $firebaseArray() or $firebaseObject() to the same data, they'll update in real-time there too.
Frank's answer is authoritative. One additional thought here is that AngularFire is extremely extensible.
If you want data pushed to two paths, you could simply override the $add method and apply the update to the second path at the same time:
app.factory('DoubleTap', function($firebaseArray, $q) {
var theOtherPath = new Firebase(...);
return $firebaseArray.$extend({
$add: function(recordOrItem) {
var self = this;
return $firebaseArray.prototype.$add.apply(this, arguments).then(function(ref) {
var rec = self.$getRecord(ref.key());
var otherData = ...do something with record here...;
return $q(function(resolve, reject) {
theOtherPath.push(rec.$id).set(otherData);
});
});
}
});
});
I have the following in my initialize file to get the values loaded in the database on startup:
Meteor.startup(function() {
if(typeof Person.findOne() === 'undefined') {
Person.insert({
name: "",
gender: ["male", "female", "prefer not to say"],
age: 0
});
}
});
And then in the server/abc.js I have:
Meteor.methods({
checkPerson: function (input) {
for (var key in Person) {
if (input === key) {
...
}
}
}
});
This meteor method checkPerson is called in the client side with a string value being passed as its only argument(input).
I want to check this 'input' string value against the name of the key in the Person Collection.
Person has a key called 'gender'. So for instance, if the 'input' holds the string value 'gender' then the if statement should be true but in my case it comes as false and hence the code inside the if statement is never executed.
Any help/guidance with this will be appreciated.
UPDATE
I searched on mongodb documentation and found here: http://docs.mongodb.org/manual/reference/operator/query/exists/ and also using some help from this thread: (using $exists in Mongo with dynamic key names and the native driver for node)
that I could do something like this:
var checkThis = {};
checkThis[input] = { $exists : true };
var p = Person.findOne(checkThis);
So if it finds one then 'p' holds the record or else it will be undefined. But still the above code does not work.
If I were to put directly:
var p = Person.find({gender: {$exists: true} });
then it works.
So I need assistance in getting the code to work with the variable 'input'.
Mongo is a schemaless database - you can insert any document structure you like into a collection and the data store won't complain. Therefore Person won't be able to indicate which fields conform to the pattern.
The most common way people deal with this problem is to use a package which provides a schema layer on top of mongo. With meteor, a popular choice is SimpleSchema, and its related package AutoForm. SimpleSchema allows you to define which fields should be allowed into a collection, and AutoForm gives you a set of helpers to enforce them in your UI.
If, instead, you prefer not to use a package you could do something like the following:
person.js
var REQUIRED_FIELDS = {
name: String,
gender: ['male', 'female', 'prefer not to say'],
age: Number
};
Person = new Meteor.Collection('person');
Person.isValid = function(person) {
try {
check(person, REQUIRED_FIELDS);
return true;
} catch (_error) {
return false;
}
};
Meteor.methods({
'person.insert': function(person) {
check(person, REQUIRED_FIELDS);
return Person.insert(person);
}
});
my-template.js
Template.myTemplate.events({
submit: function() {
var person = {
name: $('#name').val(),
gender: $('#gender').val(),
age: parseInt($('#age').val(), 10)
};
if (Person.isValid(person))
Meteor.call('person.insert', person);
else
alert('invalid person');
}
});
Here we are using meteor's check package to do some basic field validation. By adding an isValid helper to the Person collection, we can validate the schema without the need for a method call. Best of all we can reuse the same check when inserting a new document.
I want to get data from a number of queries on the same collection, and unfortunately this is not yet supported on meteor. That's why I tried to do something like this:
Common
Dep = new Deps.Dependency;
Server
Meteor.methods({
fetch: function(){
var results = Data.find(dataQuery).fetch();
var otherResults = Data.find(queryThatCannotBeCombinedWithPrevious).fetch();
return results.concat(otherResults);
},
save: function(data){
Data.insert(data);
Dep.changed();
}
update: function(data){
Data.update({_id: data._id}, data);
Dep.changed();
}
});
Client
Session.setDefault('combinedData', []);
Template.demo.data = function(){
Dep.depend();
Meteor.call('fetch',function(error, data){
Session.set('combinedData', data);
});
return Session.get('combinedData');
};
This doesn't work though, propably because the Dep variable on the client is different from the Dep on the server. Is there a way to make the method call reactive when the contents of the Data collection change?
Notes
I am currently using Meteor 0.8.1.1, which doesn't allow subscriptions that return multiple cursors of the same collection yet.
This requires a small hack and you're close. First, you need a client–only dependence, the server just passes a data returned from the method and doesn't share variables (also there's nothing on the server that requires deps). Second, you only want to fetch the actual variable once, otherwise you'll end up with an infinite loop.
Example implementation:
var value = null;
var valueInitialized = false;
var valueDep = new Deps.Dependency();
Template.demo.data = function() {
valueDep.depend();
if(!valueInitialized) {
valueInitialized = true;
Meteor.call('fetchData', function(err, result) {
value = result;
valueDep.changed();
});
}
return value;
};
I'm trying to come up with a nice way to wrap each user I fetch from the Meteor Accounts Collection in a function, including a few prototype helper functions and counts from other collections, etc. The best way to describe this is in code.
The User function I want to wrap each user in would look something like this:
// - - - - - -
// USER OBJECT
// - - - - - -
var currentUser = null; // holds the currentUser object when aplicable
function User(fbId) {
var self = this,
u = (typeof id_or_obj == 'string' || id_or_obj instanceof String ? Meteor.users.findOne({'profile.facebook.id': id_or_obj}) : id_or_obj);
self.fb_id = parseInt(u.profile.facebook.id, 10),
// Basic info
self.first_name = u.profile.facebook.first_name,
self.last_name = u.profile.facebook.last_name,
self.name = u.name,
self.birthday = u.birthday,
self.email = u.profile.facebook.email,
// Quotes
self.likeCount = Likes.find({fb_id: self.fb_id}).count() || 0;
}
// - - - - - - -
// USER FUNCTIONS
// - - - - - - -
User.prototype = {
// Get users avatar
getAvatar: function() {
return '//graph.facebook.com/' + this.fb_id + '/picture';
},
getName: function(first_only) {
return (first_only ? this.first_name : this.name);
}
};
I can easily have a global 'currentUser' variable, that holds this information about the currently logged in user on the client side like this:
Meteor.autorun(function() {
if (Meteor.user()) {
currentUser = new User(Meteor.user().profile.facebook.id);
}
});
It's also easy to implement this into a Handlebars helper, replacing the use of {{currentUser}} like this:
Handlebars.registerHelper('thisUser', function() {
if (Meteor.user()) {
return new User(Meteor.user());
} else {
return false;
}
});
What I want to do in addition to this is to make it so that when Meteor returns Meteor.user() or Meteor.users.find({}).fetch(), it includes these helper functions and short handles for first_name, last_name, etc.
Can I somehow extend Meteor.user() or is there some way to do this?
In Meteor 0.5.8, you can just tack on a transform function like so:
Meteor.users._transform = function(user) {
// attach methods, instantiate a user class, etc.
// return the object
// e.g.:
return new User(user);
}
You can do the same with non-user collections, but you can also do it this way when you instantiate the Collection:
Activities = new Meteor.Collection("Activities", {
transform: function (activity) { return new Activity(activity); }
});
(this way doesn't seem to work with the 'special' users collection)
You can use meteor package universe collection
And do like that:
UniUsers.UniUser.prototype = {
getAvatar: function() {
return '//graph.facebook.com/' + this.fb_id + '/picture';
}
};
var user = UniUsers.findOne();
console.log(user.getAvatar());
UniUsers is extended collection of Meteor.users