I'm trying to insert some things into my Firebase database using a user's uid but it comes out undefined for some reason. Take a look at the code below:
The main controller that sets the user's data information (authData) when the page loads:
flickrApp.controller('mainCtrl', ['$scope', '$rootScope', '$firebase', 'Auth', 'shared', function($scope, $rootScope, $firebase, Auth, shared) {
Auth.$onAuth(function(authData) {
shared.setAuth(authData);
$scope.authData = shared.getAuth();
});
}]);
The service which handles the authentication state and shares it across my controllers:
flickrApp.service('shared', function() {
var authentication = false;
return {
getAuth: function () {
return authentication;
},
setAuth: function (auth) {
authentication = auth;
}
};
});
Here is where it doesn't work, in my tags controller. The $scope.authData is being set correctly in the $watch function but when I try to use it in the var ref line it says that $scope.authData is undefined (so therefore I can't access the uid). I can't figure out why this isn't working as it should be..
Do I have to use $apply with the watcher function as well or what is wrong?
flickrApp.controller('tagsCtrl', ['$scope', '$rootScope', '$firebase', 'shared', function($scope, $rootScope, $firebase, shared) {
$scope.tagsList = [];
$scope.shared = shared;
$scope.$watch('shared.getAuth()', function(authData) {
$scope.authData = authData;
console.log($scope.authData);
});
var ref = new Firebase ('https://flickr.firebaseio.com/users/' + $scope.authData.uid);
var sync = $firebase(ref);
$scope.addTag = function(tag) {
$scope.tagsList.push(tag);
sync.$set({favoriteTags: $scope.tagsList});
}
}]);
I think the issue is, assignment to ref is being done before the data of $scope.authData is set in $watch. Try to change your code to this:
flickrApp.controller('tagsCtrl', ['$scope', '$rootScope', '$firebase', 'shared', function($scope, $rootScope, $firebase, shared) {
$scope.tagsList = [];
$scope.shared = shared;
var ref,sync;
$scope.$watch('shared.getAuth()', function(authData) {
$scope.authData = authData;
console.log($scope.authData);
if($scope.authData){
ref = new Firebase ('https://flickr.firebaseio.com/users/' + $scope.authData.uid);
sync = $firebase(ref);
}
});
$scope.addTag = function(tag) {
$scope.tagsList.push(tag);
sync.$set({favoriteTags: $scope.tagsList});
}
}]);
Related
I'm trying to retrieve data from Angularfire using a service, and then setting the returned value to my scope in my controller.
When I run the code below, I get undefined back for scope.sessions.
SERVICE:
app.factory('sessions', function(){
var refToSessions = new Firebase('myFireBaseURL');
var allSessions = [];
return {
getSessions: function () {
refToSessions.on("value", function (sessions) {
allSessions.push(sessions.val());
return allSessions;
});
}
};
});
CONTROLLER:
app.controller('SessionsCtrl', ['$scope', '$state', 'Auth', 'sessions', function($scope, $state, Auth, sessions){
$scope.sessions = sessions.getSessions();
$scope.submitSession = function() {
console.log($scope.sessions);
}
});
You're trying to return asynchronous data.
You are logging allSessions to the console before the data has downloaded from Firebase.
Use $firebaseArray from AngularFire.
app.constant('FirebaseUrl', '<my-firebase-url>');
app.service('rootRef', ['FirebaseUrl', Firebase);
app.factory('Sessions', function(rootRef, $firebaseArray){
var refToSessions = ref.child('sessions');
return $firebaseArray('sessions');
}
Then injection Sessions into your controller:
app.controller('SessionsCtrl', function($scope, $state, Auth, Sessions){
$scope.sessions = Sessions; // starts downloading the data
console.log($scope.sessions); // still empty
$scope.submitSession = function() {
// likely by the time you click here it will be downloaded
console.log($scope.sessions);
$scope.sessions.$add({ title: 'new session' });
};
});
The data starts downloading once it's injected into your controller. When it's downloaded, $firebaseArray knows to trigger $digest, so it appears on the page.
Since you're using ui-router, you can use resolve to make sure the data exists before injecting it into your controller:
app.config(function ($stateProvider) {
$stateProvider
.state("session", {
controller: "SessionsCtrl",
templateUrl: "views/sessions.html",
resolve: {
sessions: function(Sessions) {
// return a promise that will fulfill the data
return Sessions.$loaded();
}
}
})
});
Now you would change your controller code to this:
app.controller('SessionsCtrl', function($scope, $state, Auth, sessions){
$scope.sessions = sessions; // data is available since injected by router
console.log($scope.sessions); // logs the appropriate data
$scope.submitSession = function() {
$scope.sessions.$add({ title: 'new session' });
};
});
I have a controller that uses AngularFire's $firebaseObject.
When the controller initializes, it sets the reference to Firebase, and uses $loaded function to verify that data from Firebase is actually there. Here is a simplified code of the controller:
myModule.controller('VotingQuestionsCtrl', ['$scope', '$rootScope', '$firebaseObject',
function($scope, $rootScope, $firebaseObject) {
var baseRef = new Firebase("https://somefirebaserepo.firebaseio.com/pathtomyobjects"),
topicsRef = baseRef.child("topics");
var topics = $firebaseObject(topicsRef);
$scope.dataLoaded = false;
topics.$loaded().then(function() {
$scope.dataLoaded = true;
topics.$bindTo($scope, 'topics').then(function() {
//do some stuff
});
}, function(error) {
$scope.dataLoaded = true;
console.log("oh no!");
});
This controller works pretty good in real life. The problem is testing it with Karma. No matter what I tried, my test spec never reaches the callback for topics.$loaded (neither the success function nor the error function).
Here is the current state of my test spec:
describe('VotingQuestionsCtrl', function() {
var $controller, $scope, $rootScope, $firebaseObject, $stateParams, $q, baseRef, $timeout;
beforeEach(module('wsApp.controllers'));
beforeEach(inject(function ($injector) {
MockFirebase.override();
$controller = $injector.get('$controller');
$q = $injector.get('$q');
$rootScope = $injector.get('$rootScope');
$timeout = $injector.get('$timeout');
$scope = $rootScope.$new();
$firebaseObject = $injector.get('$firebaseObject');
$rootScope.user = {username: "user"};
$rootScope.selectedSim = {phase: "phase1"};
baseRef = new Firebase("https://somefirebaserepo.firebaseio.com/pathtomyobjects");
baseRef.set({topics: {simId: {phase1: "bla"}}, users: {}});
baseRef.flush();
}));
it('loads the controller', function(done) {
$controller('VotingQuestionsCtrl', {$scope: $scope, $rootScope: $rootScope, $firebaseObject: $firebaseObject});
$scope.$digest();
setTimeout(function() {
expect($scope.dataLoaded).toBeTruthy();
done();
}, 200);
});
});
I've tried all sorts of flushing, digesting, whatever... the test goes through the rest of the controller, but the async part just doesn't work.
Say i have the following factory:
app.factory("categoryFactory", function (api, $http, $q) {
var selected = null;
var categoryList = [];
return {
getList: function () {
var d = $q.defer();
if(categoryList.length <= 0){
$http.get(api.getUrl('categoryStructure', null))
.success(function (response) {
categoryList = response;
d.resolve(categoryList);
});
}
else
{
d.resolve(categoryList)
}
return d.promise;
},
setSelected: function (category) {
selected = category;
},
getSelected: function () {
return selected;
}
}
});
now i have two controllers using this factory at the same time. Because of this both controllers has to be notified when updated for this i attempted the following:
app.controller('DashboardController', ['$http', '$scope', '$sessionStorage', '$log', 'Session', 'api','categoryFactory', function ($http, $scope, $sessionStorage, $log, Session, api, categoryFactory) {
$scope.selectedCategory = categoryFactory.getSelected();
}]);
While my other controller looks like this:
app.controller('NavController', ['$http', '$scope', '$sessionStorage', '$log', 'Session', 'api', 'FileUploader', 'categoryFactory', function ($http, $scope, $sessionStorage, $log, Session, api, FileUploader, categoryFactory) {
$scope.categories = [];
categoryFactory.getList().then(function (response) {
$scope.categories = response;
});
$scope.selectCategory = function (category) {
categoryFactory.setSelected(category);
}
}]);
how ever when the NavController changed the value it was not changed in the DashboardController
My question is how can i either watch or in another way get notified when the value changes?
You can use an observer pattern, like so:
app.factory("categoryFactory", function (api, $http, $q) {
// the list of callbacks to call when something changes
var observerCallbacks = [];
// ...
function notifyObservers() {
angular.forEach(observerCallbacks, function(callback) {
callback();
});
}
return {
setSelected: function (category) {
selected = category;
// notify the observers after you change the value
notifyObservers();
},
registerObserver: function(callback) {
observerCallbacks.push(callback);
}
}
});
And then in your controllers:
app.controller('NavController', ['$http', '$scope', '$sessionStorage', '$log', 'Session', 'api', 'FileUploader', 'categoryFactory', function ($http, $scope, $sessionStorage, $log, Session, api, FileUploader, categoryFactory) {
// ...
// init
(function() {
categoryFactory.registerObserver(function() {
categoryFactory.getList().then(function (response) {
$scope.categories = response;
});
});
})();
}]);
This way, any time setSelected is called, it calls each callback that you've registered in observerCallbacks. You can register these from any controller since factories are singletons and they will always be in the know.
Edit: just want to add that I may have put the notifyObservers() call in the wrong area (currently in setSelected) and that I may be putting the wrong update call in the controller (currently getList) but the architecture remains the same. In the registerObserver, put whatever you want to do when the values are updated and wherever you make changes that you want observers to know about call notifyObservers()
You could follow dot rule here so that prototypal inheritance will get followed.
Basically you need to have one object inside your service that will have selected variable, And will get rid of getSelected method.
Factory
app.factory("categoryFactory", function(api, $http, $q) {
var categoryFactory = {};
categoryFactory.getList = function() {
var d = $q.defer();
if (categoryList.length <= 0) {
$http.get(api.getUrl('categoryStructure', null))
.success(function(response) {
categoryList = response;
d.resolve(categoryList);
});
} else {
d.resolve(categoryList)
}
return d.promise;
}
categoryFactory.setSelected = function(category) {
categoryFactory.data.selected = category;
}
categoryFactory.data = {
selected: null
}
return categoryFactory;
});
Controller
app.controller('DashboardController', ['$http', '$scope', '$sessionStorage', '$log', 'Session', 'api', 'categoryFactory',
function($http, $scope, $sessionStorage, $log, Session, api, categoryFactory) {
//this will provide you binding without watcher
$scope.selection = categoryFactory.data;
}
]);
And then use {{selection.selected}} on html part will update a value when changes will occur in selection.
I'm trying to call the mergeUserList() function that is inside my service. I do this is my controller that looks like this:
app.controller('UserManagementController', ['$http','$sessionStorage','api','$modal','$scope','Session', 'divisionService','$filter', function ($http, $sessionStorage, api, $modal, $scope, $state, Session, divisionService,$filter) {
divisionService.mergeUserList().then(function(response)
{
$scope.users = response;
});
}]);
And my service:
app.factory("divisionService", function (api, $http, $q) {
//Organization divisions with division users
var division = {};
var divisionArray = [];
var mergedUserList = [];
return {
mergeUserList: function () {
if (divisionArray == null || divisionArray.length == 0) {
var list = [];
var d = $q.defer();
this.getList().then(function () {
divisionArray.forEach(function (y) {
y.users.forEach(function (user) {
list.push(user);
});
d.resolve(list);
})
});
return d.promise;
}
else {
return null;
}
}
};
return division;
});
My problem is that when i run the code it says TypeError: undefined is not a function in line 1 in the controller. I know for a fact that the problem is not in the service, becuase I use it in another controller, and there it works.
You have one $state as a function argument which is not included in the array, change it to:
app.controller('UserManagementController', ['$http','$sessionStorage','api','$modal','$scope', '$state', 'Session', 'divisionService','$filter', function ($http, $sessionStorage, api, $modal, $scope, $state, Session, divisionService, $filter) {
I've been doing quite a lot of reading about angular dependency injection and factories vs services etc like in this post here - angular.service vs angular.factory
I'm struggling putting it into practise and wonder if you can give me suggestions on how you would do it.
My current code looks like this
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
function mainController($scope, $firebase) {
var db = new Firebase("https://**.firebaseio.com/");
$scope.messages = $firebase(db);
$scope.addItem = function(error) {
if (error.keyCode != 13) return;
$scope.messages.$add({ name: $scope.name, price: $scope.price });
$scope.name = "";
$scope.price = "";
};
}
I decided I wanted to use angular routes and split this basic function up into two different controllers that I would use for my test app. the MainController would just display everything in the firebase db and the AdminController would be able to add messages to it
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
.factory('fireBaseConnectionService', $firebase)
//code in here to connect to firebase and add messages
.controller('MainController', function(fireBaseConnectionService, $scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
//code here to retrieve everything from firebase db
})
.controller('AdminController', function(fireBaseConnectionService, $scope, $routeParams) {
$scope.name = "AdminController";
$scope.params = $routeParams;
//code here to add a row to the db
})
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/', {
redirectTo: '/menu'
})
.when('/menu', {
path: '/menu',
templateUrl: 'partials/menu.html',
controller: 'MainController'
})
.when('/admin', {
templateUrl: 'partials/admin.html',
controller: 'AdminController'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(false);
});
My problem is I don't want to have to connect to the firebase db in each controller. I would like to have a factory that handles this for me and has maybe functions within that that I can call from my controllers to view everything in db and to add something to the db
factory()
As we’ve seen, the factory() method is a quick way to create and configure a service.
The factory() function takes two arguments:
• name (string)
This argument takes the name of the service we want to register.
• getFn (function)
This function runs when Angular creates the service.
angular.module('myApp')
.factory('myService', function() {
return {
'username': 'auser'
}
});
The getFn will be invoked once for the duration of the app lifecycle, as the service is a singleton
object. As with other Angular services, when we define our service, getFn can take an array or a
function that will take other injectable objects.
The getFn function can return anything from a primitive value to a function to an object (similar to
the value() function).
angular.module('myApp')
.factory('githubService', [
'$http', function($http) {
return {
getUserEvents: function(username) {
// ...
}
}
}]);
service()
If we want to register an instance of a service using a constructor function, we can use service(),
which enables us to register a constructor function for our service object.
The service() method takes two arguments:
• name (string)
This argument takes the name of the service instance we want to register.
• constructor (function)
Here is the constructor function that we’ll call to instantiate the instance.
The service() function will instantiate the instance using the new keyword when creating the
instance.
var Person = function($http) {
this.getName = function() {
return $http({
method: 'GET',
url: '/api/user'
});
};
};
angular.service('personService', Person);
provider
These factories are all created through the $provide service, which is responsible for instantiating
these providers at run time.
angular.module('myApp')
.factory('myService', function() {
return {
'username': 'auser'
}
})
// This is equivalent to the
// above use of factory
.provider('myService', {
$get: function() {
return {
'username': 'auser'
}
}
});
Why would we ever need to use the .provider() method when we can just use the .factory()
method?
The answer lies in whether we need the ability to externally configure a service returned by the
.provider() method using the Angular .config() function. Unlike the other methods of service
creation, we can inject a special attribute into the config() method.
from ng-book
All you have to do is just move the firebase connection into the service, and inject that service wherever you want . The connection line will execute the first time your app runs, given that you front load the service when your app runs, as you seem to be doing now:
.factory('fireBaseConnectionService', function($firebase){
var db = $firebase(new Firebase("https://**.firebaseio.com/"));//creating
//the firebase connection this line executes only once when the service is loaded
return{
getMessage:function(){
return db.whatever;
}
}
})
If you load the service script dynamically, on route where you need it, it will only connect to the database when it reaches that route. The code above will create one connection to the database, as the connection line is executed only once.
Just for anyone interested with the help of the answers above and this link - Firebase _ AngularJS this is what I ended up doing
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
.factory('fireBaseConnectionService', ["$firebase", function($firebase) {
var db = new Firebase("https://***.firebaseio.com/");
return {
getMessages: function() {
return $firebase(db);
},
addMessage: function(message) {
var messages = $firebase(db);
messages.$add(message);
}
}
}])
.controller('MainController', ["fireBaseConnectionService", "$scope", function (fireBaseConnectionService, $scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
$scope.messages = fireBaseConnectionService.getMessages();
}])
.controller('AdminController', ["fireBaseConnectionService", "$scope", function(fireBaseConnectionService, $scope, $routeParams) {
$scope.name = "AdminController";
$scope.params = $routeParams;
$scope.addItem = function(error) {
if (error.keyCode != 13) return;
fireBaseConnectionService.addMessage({ name: $scope.name, price: $scope.price });
$scope.name = "";
$scope.price = "";
}
}])
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/', {
redirectTo: '/menu'
})
.when('/menu', {
path: '/menu',
templateUrl: 'partials/menu.html',
controller: 'MainController'
})
.when('/admin', {
templateUrl: 'partials/admin.html',
controller: 'AdminController'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(false);
});