I'm just starting out with AngularFire and can't seem to get the three-way binding happening.
In my controller I have:
$scope.item = Item.find($stateParams.id);
and in the item service I have
.factory('Item', ['$firebase','FIREBASE_URL', function ($firebase, FIREBASE_URL) {
var ref = new Firebase(FIREBASE_URL + 'items');
var Item = {
find: function(id) {
return $firebase(ref.child(id)).$asObject();
}
};
return Item;
}]);
I then have a input in my view that gets updated with the right item value from Firebase but when I type in textbox it doesn't update Firebase. Any ideas?
UPDATE:
I have a solution but it would be nice to know whether this is the way to do it.
I have an ng-change in my textbox which calls a function in my Controller. The contoller then calls $scope.item.$save($scope.item.text);
To synchronize the object changes automatically back to Firebase, you have to call $firebase(ref.child(id)).$asObject().$bindTo($scope, "data");. That sets up the binding you are looking for.
Both approaches have their merits. When you explicitly trigger the ng-change event, you have more control over when the data gets saved. On the other hand: using $bindTo is more of a set-it-and-forget-it approach.
Related
I forked an example from Angular 1's tutorial:
https://plnkr.co/edit/I48XFq
2 controllers (MainCtrl and AltCtrl) reference to the same Hero data from one dataService
MainCtrl and AltCtrl use a heroDetail component to render view of data
My setup:
dataService udpates data.time every 3 seconds using setInterval (I'm trying to not use Angular's $interval to get data rendered on view)
.factory('dataService', function() {
var data = {};
data.location = "Safe House";
data.count = 0;
data.time = new Date();
setInterval(function() {
data.time = new Date();
data.count++;
}, 1000);
data.updateLocation = function(origin) {
if (origin === 'click') return;
data.location = "Safe House #" + data.count;
}
return data;
});
In the heroDetail view, I put a button that invoke dataService.updateLocation('click'). Invoking from this button will do nothing, just return.
Also in the heroDetail controller, there's a setInterval to call dataService.updateLocation('setInterval') that actually update data.location
function HeroDetailController(dataService) {
var $ctrl = this;
$ctrl.update = function() {
dataService.updateLocation('click');
}
setInterval(function() {
dataService.updateLocation('setTimeout');
}, 3000); }
Result:
The service's data though gets udpate via background setInterval, but is not rendered on component view
But when I click on the front-end button, data is rendered with latest udpate, dispite the button just do nothing on data.
Could you help to explain why and how data got updated from service to the view in this case?
Thank you!
Using built-in $interval service instead of window.setInterval may solve your problem. AngularJs $interval documentation
To understand the problem, first read about JavaScript event loop. Event loop explained here
AngularJS $digest loop is based on event loop and after each digest cycle, it updates DOM according to the model that is two-way bound to the view. $digest cycle explained here
I think that you should add to your "setInterval" function the following line of code:
data.location = "Safe House #" + data.count;
Hope this helps.
I have an app that has items, and you can do things like add new items, update the text of an item, move the item to a different folder, etc.
I have an items factory that holds all the items as plain objects inside an array, and the factory returns a singleton that has various methods, like get(), set(), etc.
To add some context to the question, I'm working with Node.js and MongoDB as well.
Anyway, due to all the various factories I have, like items, folders, and all the various controllers for different views, I am relying heavily on events. To give some examples:
// items factory
update: function(params) {
// add to database, then...
.then(function() {
$rootScope.$emit('itemCreated');
});
}
// items controller
// I need to refresh the items list in the scope
$rootScope.$on('itemCreated', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
These are their own kind of "subset" of events, in that they all pertain to "CRUD" operations on items.
But, I also have other events that I use. For example, I have an interceptor that listens to any requests and responses. I have a loading widget (an image of a spinning wheel) that uses a directive. This directive will show the loading widget when a request begins, and hide the loading widget when a request ends. This is also event based.
// on request
$rootScope.$emit(_START_REQUEST_);
// on any response
$rootScope.$emit(_END_REQUEST_);
I attempted to "modularize" these request and response events by simply making them constants.
.constant('_START_REQUEST_', '_START_REQUEST_');
I am trying to find a solution in order to "modularize" all my other events, like the events emitted on CRUD operations for items. One idea I've had is to define all of the item CRUD events inside the items factory:
events: {
update: 'itemUpdate',
create: 'itemCreated'
// etc.
}
Then, I can simply inject my items factory into a controller, and reference events like so:
$rootScope.$on(items.events.update, function() {});
I also considered simply defining all events, regardless of whether they are interceptor events or item events, as constants in my app. However, it seemed like this solution directly coupled item events to the module itself, rather than to the items factory, which is where I feel they "belong".
Basically, the issue is that right now all my events definitions seem to be scattered around. My question is: what pattern or best practice would you recommend for modularizing and defining events in AngularJS?
I agree that these item events should belong to the event source. You could implement a observer pattern in the item factory that hides the dependency on $rootScope for event listeners. This way the event key itself is a private detail of the item factory, and the subscription to the event is made explicit by calling a dedicated function for it. This approach makes your code more independent of $rootScope and easier to maintain than an event name convention (thinking about usages search for the specific event subscription method vs. usages of $rootScope.$emit / $on):
angular.module('events', [])
.service('items', ['$rootScope', function($rootScope) {
var createdEventKey = 'item.created';
return {
create: function () {
$rootScope.$emit(createdEventKey, {"name": "aItemName"});
},
onCreated: function(callback, scope) {
var unsubscribeFunction = $rootScope.$on(createdEventKey, function(event, payload) {
callback(payload);
});
// allow to unsubscribe automatically on scope destroy to prevent memory leaks
if (scope) {
scope.$on("$destroy", unsubscribeFunction);
}
return unsubscribeFunction;
}
}
}])
.controller('TestController', function($scope, items) {
items.onCreated(function (item) {
console.log("Created: " + item.name);
}, $scope);
});
complete example: http://jsfiddle.net/8LtyB/32/
If all you want is a way to create a separate object for containing the names of events, why not use a service?
myApp.service('itemEvents', function () {
var events = {
update: 'itemupdate',
create: 'itemcreate',
...
};
return events;
});
This is essentially what you had before when you were suggesting using a factory to contain the event definitions, except that a service is a single object instance, and is instantiated at module start-up. In contrast, a factory creates a new instance when injected into a controller. (Here's a good SO post on the difference between services and factories)
You can inject this service into your controllers or directives:
myApp.controller('ItemController', function ($scope, itemEvents) {
$scope.on(itemEvents.update, function () { /* something interesting */ });
});
This gives you a nice place to centralize your event name definitions. As a side note, some people hold to the convention of using all lowercase when defining event names (so itemupdate instead of itemUpdate). Hope this helps!
You can use the following:
app.config(function($provide) {
$provide.decorator("$rootScope", function($delegate) {
var Scope = $delegate.constructor;
var origBroadcast = Scope.prototype.$broadcast;
var origEmit = Scope.prototype.$emit;
Scope.prototype.$broadcast = function() {
console.log("$broadcast was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origBroadcast.apply(this, arguments);
};
Scope.prototype.$emit = function() {
console.log("$emit was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origEmit.apply(this, arguments);
};
return $delegate;
});
})
example: http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview
src: https://github.com/angular/angular.js/issues/6043
assuming these $scope.$emit works like jquery events I would suggest you name your emits to be generic for example in you database update simply do this:
$rootScope.$emit('Created')
then in your items controller do this :
$rootScope.$on('Created.item', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
then you can wire to the created event in any of your controllers and its name is generic. The .item should add a namespace. if you make all of your events in your items controller have the .item name space you should be able to do a
$rootScope.$off('item')
This will clear up memory leaks
How can I switch out a service on-the-fly and have all components (relying on the service) automatically be bound to the data on the new strategy?
I have a Storage service and two storage strategies, StorageStrategyA and StorageStrategyB. Storage provides the public interface to controllers and other components to interact with:
angular.module('app').factory('Storage', function ($injector) {
var storage;
var setStrategy = function (name) {
storage = $injector.get(name);
};
setStrategy('StorageStrategyB');
return {
getItems: function () {
return storage.getItems();
}
// [...]
};
});
But when the strategy is changed, the two-way binding breaks and the view doesn't update with items from getItems() from the new strategy.
I've created a Plunker to illustrate the problem.
Is there a way to combine the strategy pattern with AngularJS and keep the two-way binding?
Please note that in my actual app I cannot just call Storage.getItems() again after the strategy has been changed, because there are multiple components (views, controllers, scopes) relying on Storage and the service change happens automatically.
Edit:
I have forked the Plunker to highlight the problem. As you can see, the data in the upper part only updates because I manually call Storage.getItems() again after the strategy has been changed. But I cannot do that, because other component - for example OtherController - are also accessing data on Storage and also need to automatically get their data from the new strategy. Instead, they stay bound to the initial strategy.
Javascript works on references. Your array items in app is same reference as items of strategyB items initially with the below statement and when you update the StrategyB items automatically items in your view gets updated(since same reference).
$scope.items = Storage.getItems();
So, when you switch strategy you are not changing the reference of items. It still points to StrategyB items reference.
You have to use the below mechanism to change the reference.
Then you can do something where you can communicate between controllers to change the items reference.
Please find the plunkr I have updated.
$rootScope.$broadcast("updateStrategy");
And then update your item list and others.
$scope.$on("updateStrategy",function(){
$scope.name = Storage.getName();
$scope.items = Storage.getItems(); //Here changing the reference.
//Anything else to update
});
the two way binding is still ok, you have a reference issue.
when the AppController set up the $scope.items set to the StorageStrategyB items, then when you switch to StorageStrategyA, the AppController $scope.items is still set to StorageStrategyB items.
angular.module('app').controller('AppController', function ($scope, Storage) {
Storage.setStrategy('StorageStrategyB');
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
$scope.setStrategy = function (name) {
Storage.setStrategy(name);
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
console.log( $scope.items);
console.log($scope.current);
};
$scope.addItem = function () {
Storage.addItem($scope.item);
$scope.item = '';
};
});
You forgot
$scope.items = Storage.getItems();
nice question :)
plnkr
I am writing a small Angular web application and have run into problems when it comes to loading the data. I am using Firebase as datasource and found the AngularFire project which sounded nice. However, I am having trouble controlling the way the data is being displayed.
At first I tried using the regular implicit synchronization by doing:
angularFire(ref, $scope, 'items');
It worked fine and all the data was displayed when I used the model $items in my view. However, when the data is arriving from the Firebase data source it is not formatted in a way that the view supports, so I need to do some additional structural changes to the data before it is displayed. Problem is, I won't know when the data has been fully loaded. I tried assigning a $watch to the $items, but it was called too early.
So, I moved on and tried to use the angularfireCollection instead:
$scope.items = angularFireCollection(new Firebase(url), optionalCallbackOnInitialLoad);
The documentation isn't quite clear what the "optionalCallbackOnInitialLoad" does and when it is called, but trying to access the first item in the $items collection will throw an error ("Uncaught TypeError: Cannot read property '0' of undefined").
I tried adding a button and in the button's click handler I logged the content of the first item in the $items, and it worked:
console.log($scope.items[0]);
There it was! The first object from my Firebase was displayed without any errors ... only problem is that I had to click a button to get there.
So, does anyone know how I can know when all the data has been loaded and then assign it to a $scope variable to be displayed in my view? Or is there another way?
My controller:
app.controller('MyController', ['$scope', 'angularFireCollection',
function MyController($scope, angularFireCollection) {
$scope.start = function()
{
var ref = new Firebase('https://url.firebaseio.com/days');
console.log("start");
console.log("before load?");
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
console.log("start() out");
};
$scope.start();
//wait for changes
$scope.$watch('items', function() {
console.log("items watch");
console.log($scope.items[0]); //undefined
});
$scope.testData = function()
{
console.log($scope.items[0].properties); //not undefined
};
}
]);
My view:
<button ng-click="testData()">Is the data loaded yet?</button>
Thanks in advance!
So, does anyone know how I can know when all the data has been loaded
and then assign it to a $scope variable to be displayed in my view? Or
is there another way?
Remember that all Firebase calls are asynchronous. Many of your problems are occurring because you're trying to access elements that don't exist yet. The reason the button click worked for you is because you clicked the button (and accessed the elements) after they had been successfully loaded.
In the case of the optionalCallbackOnInitialLoad, this is a function that will be executed once the initial load of the angularFireCollection is finished. As the name implies, it's optional, meaning that you don't have to provide a callback function if you don't want to.
You can either use this and specify a function to be executed after it's loaded, or you can use $q promises or another promise library of your liking. I'm partial to kriskowal's Q myself. I'd suggest reading up a bit on asynchronous JavaScript so you get a deeper understanding of some of these issues.
Be wary that this:
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
does correctly specify a callback function, but $scope.items doesn't get assigned until after you've ran the callback. So, it still won't exist.
If you just want to see when $scope.items has been loaded, you could try something like this:
$scope.$watch('items', function (items) {
console.log(items)
});
In my project I needed to know too when the data has been loaded. I used the following approach (implicit bindings):
$scope.auctionsDiscoveryPromise = angularFire(firebaseReference.getInstance() + "/auctionlist", $scope, 'auctionlist', []);
$scope.auctionsDiscoveryPromise.then(function() {
console.log("AuctionsDiscoverController auctionsDiscoveryPromise resolved");
$timeout(function() {
$scope.$broadcast("AUCTION_INIT");
}, 500);
}, function() {
console.error("AuctionsDiscoverController auctionsDiscoveryPromise rejected");
});
When the $scope.auctionsDiscoveryPromise promise has been resolved I'm broadcasting an event AUCTION_INIT which is being listened in my directives. I use a short timeout just in case some services or directives haven't been initialized yet.
I'm using this if it would help anyone:
function getAll(items) {
var deferred = $q.defer();
var dataRef = new Firebase(baseUrl + items);
var returnData = angularFireCollection(dataRef, function(data){
deferred.resolve(data.val());
});
return deferred.promise;
}
I am using AngularJS and FireBase in my application. I bound an object to be in sync with FireBase:
$scope.winnerPromise = angularFire(travelBidsFirebaseRef + "/user/" + $scope.auction.winnerUserId, $scope, 'winner', {});
Now I want to disassociate $scope.winner, meaning I want it to stay safe in the FireBase DB, but I don't want my scope variable 'winner' to be synchronized with it anymore. How do I do that? I saw disassociate() finction in angularfire.js but I don't see how I can use it. Any ideas?
The disassociate function is passed to you when the promise is resolved. I'd use it as follows:
var ref = travelBidsFirebaseRef.child("user/" + $scope.auction.winnerUserId);
var promise = angularFire(ref, $scope, "winner", {});
promise.then(function(disassociate) {
// Do some work...
disassociate(); // Don't synchronize $scope.winner anymore.
});
Hope this helps!
I am using angularfire and for me it worked the $destroy method.
https://github.com/firebase/angularfire/blob/master/docs/reference.md#destroy-1