Is there a way to watch when one property in an object changes? I tried
var unwatch = obj.$watch(function() {
console.log("data changed");
});
That fired when any property in the object changes. I tried
var unwatch = obj.myProperty.$watch(function() {
console.log("data changed");
});
That returned an error message: "TypeError: obj.myProperty.$watch is not a function".
I tried
var myTargetValue = $firebaseObject(ref.myProperty);
That returned an error message: "TypeError: Cannot read property 'ref' of undefined".
You'll have to create a $firebaseObject for the property. But, using the Firebase SDK tends to be more useful than $watch().
JSBin Demo
angular.module('app', ['firebase'])
.constant('FirebaseUrl', 'https://34474165.firebaseio-demo.com/')
.service('rootRef', ['FirebaseUrl', Firebase])
.controller('MyCtrl', MyController);
function MyController($scope, $firebaseObject, rootRef) {
var objRef = rootRef.child('obj');
var obj = $firebaseObject(objRef);
var objMessage = $firebaseObject(rootRef.child('obj/message'));
// watch the whole object
var unwatch = obj.$watch(function(a) {
console.log(a);
});
// watch a child property
objMessage.$watch(function(a) {
console.log(a, 'msg');
});
// get the whole object as it changes
objRef.on('value', function(snap) {
console.log(snap.val());
});
// get the child property as it changes
objRef.child('message').on('value', function(snap) {
console.log(snap.val());
});
}
Short answer - Use vanilla Firebase (i.e. don't use Angularfire)
To watch a string, number or boolean property with Angularfire it's best to use vanilla Firebase:
// firebase reference
db = firebase.database().ref().child("myTable");
// watch the property
db.child("someProperty").on("value", function(snapshot) {
$scope.message = snapshot.val();
});
Why not Angularfire, and $value:
Using $firebaseObject and passing in a reference to a primitive (string, number, boolean) returns an object like the following (reference documentation):
{
$id: "myProperty",
$priority: null,
$resolved: true,
$value: "Hi mom!"
}
In this example, the string I'm trying to watch is contained in the $value property of the returned object. I was inclined to assign the $value property to the $scope, however this wouldn't work:
// WONT WORK
$scope.foo = $firebaseObject(db.child("myProperty")).$value;
There's a clash between Angular's object naming conventions and Firebase's object naming conventions that causes some trouble here. According to this "Important Note" in the documentation, Angular's $watch function ignores properties with a $ prefix. In other words, the view is not going to update if you assign the $value property of the returned object to $scope.
Thus, it's best to just use vanilla firebase to solve this problem (see top). Hope people find this helpful.
Related
I am trying to set a value using autoValue based on already stored values
I am using meteor 1.3.4.1
This used to work in meteor 1.1.0.2
here is my code:
id: {
type: String,
label: "ID",
autoValue: function() {
var isFirstTime = this.field("profile.isFirstTime").value;
var isApproved = this.field("profile.changesApproved").value;
var value = this.field("profile.unapproved_id").value;
var userId = this.userId;
var user = Meteor.users.findOne({_id: userId});
if (user && user.profile && user.profile.id)
{
return user.profile.id;
}
}
}
I expect value of user.profile.id to be returned since user.profile.id has a value in the users collection but I get a value that is passed from input field. How do I get simple-schema to notice collection values as it used to on meteor 1.1.0.2
I think the confusing part here is the 'this' keyword. in the context of the autoValue function(), this may be the object itself, unless some other context is bound to it by SimpleSchema.
So would suggest you trace the code with debugger and check what is the value of this. if it is not what you think it is, check where it is defined
in my custom Polymer element, i have a method that queries a firebase instance like so:
_updateUser: function(user) {
if(this._firebaseRef) {
this._firebaseRef.off()
}
if(user) {
var userLocation = ['xxxxx.firebaseio.com', 'users', this.user.uid].join('/');
this._firebaseRef = new Firebase(userLocation);
var query = this._firebaseRef.orderByChild("username")
query.on("value", function(messageSnapshot) {
messageSnapshot.forEach(function(messageData){
this.set('usernames', []);
console.log(this);
});
},null, this);
}
},
However, I keep getting the following error Uncaught TypeError: this.set is not a function. It seems that this is not set as the context as expected as per the api documentation
May I know how I can call this.set in my callback successfully? Thanks.
You need to pass the context into the forEach function as well.
messageSnapshot.forEach(function (messageData) {...}, this)
or
messageSnapshot.forEach(function (messageData) {...}.bind(this))
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.
Consider the following code :
Template.fullDoc.rendered = function() {
// Get triggered whenever the selected document id changes
this.autorun(function() {
var docId = isolateValue(function() {
return Template.currentData().selectedDoc._id;
});
...
});
}
This code doesn't work. Inside isolateValue(), Template.currentData() sometimes triggers an exception: Exception from Tracker recompute function: Error: There is no current view (this corresponds to the fact that Template.instance() returns null).
So how do you set a reactive dependency on a subpart of a template data context?
You could recreate the isolateValue behaviour in a way which doesn't cause Template.instance() to get set to null sometimes.
$ meteor add reactive-var
Template.fullDoc.rendered = function () {
var docIdVar = new ReactiveVar();
this.autorun(function () {
docIdVar.set(Template.currentData().selectedDoc._id);
});
this.autorun(function () {
var docId = docIdVar.get();
// ...
});
}
This makes use of the fact that setting a ReactiveVar to the same value it already has will not trigger an invalidation. (By default this only works for primitives; for objects you'll need to pass a custom equalsFunc when you construct the ReactiveVar. If _id is a string, you're fine. If it's ObjectID you probably aren't.)