This question already has an answer here:
How come Angular doesn't update with scope here?
(1 answer)
Closed 6 years ago.
I have a Firebase function inside an angular controller. There is a button that when clicked takes the selected option and the type input and stores them into the user's object like so:
{
selected-option : input-value
}
This works perfectly, but only works when the view is changed. In my case both airlines already have data so this function displays an $ionicPopup.
After the view has changed once the functionality is absolutely perfect. This is obviously a problem and I assume it is an $apply or $digest issue.
Here is my controller code (Supected location marked by "ISSUE RIGHT HERE"):
.controller('ProgramCtrl', ['$scope', '$state', '$firebaseArray', 'facebook', '$ionicPopup', '$ionicLoading',
function($scope, $state, $firebaseArray, facebook, $ionicPopup, $ionicLoading) {
$scope.goBack = function() {
$state.go('app.home');
}
$scope.show = function() {
$ionicLoading.show({
template: 'Loading...'
});
};
$scope.hide = function(){
$ionicLoading.hide();
};
// Get who is logged in
$scope.user = facebook.get();
// Array of airlines
var airRef = ref.child("airlines");
$scope.airlines = $firebaseArray(airRef);
$scope.selectedAir = {};
$scope.miles = {};
$scope.revealInput = function(num) {
// Jquery variables
$milesPoints = $(".milesPoints");
$saveTicket = $(".saveTicket");
// Will fade in visibility depending on num passed into function
switch(num) {
case 1:
$saveTicket.prop("disabled", false);
$saveTicket.fadeTo(400, 1);
break;
default:
break;
}
}
// Add program to user
$scope.addProgram = function () {
// Connect to Firebase
Firebase.goOnline();
// Check for facebook user
if(jQuery.isEmptyObject($scope.user)) {
// Get Firebase user
var authData = ref.getAuth();
var theUser = ref.child("users").child(authData.uid);
var selected = {};
// Access user miles data
// $scope.show();
// ISSUE RIGHT HERE
theUser.child("miles").child($scope.selectedAir.name.$id).once("value", function(snapshot) {
// Update scopes
var exist = snapshot.exists();
// Check if object id exists, if so notify user
if(!exist) {
// Save and update program to user object
selected[$scope.selectedAir.name.$id] = $scope.miles.num;
theUser.child("miles").update(selected);
//$scope.hide();
$state.go("app.saved");
} else {
// Popup alert
var alertPopup = $ionicPopup.alert({
title: 'Oops!',
template: "You already created this airline! Go to the 'Add Ticket' page to add more points."
});
alertPopup.then(function(res) {
console.log("You already created this airline! Go to the 'Add Ticket' page to add more points.");
});
}
})
} else {
var theUser = ref.child("users").child($scope.user.id);
var selected = {};
$scope.show();
theUser.child("miles").child($scope.selectedAir.name.$id).once("value", function(snapshot) {
var exist = snapshot.exists();
if(!exist) {
selected[$scope.selectedAir.name.$id] = $scope.miles.num;
theUser.child("miles").update(selected);
$scope.hide();
$state.go("app.saved");
} else {
var alertPopup = $ionicPopup.alert({
title: 'Oops!',
template: "You already created this airline! Go to the 'Add Ticket' page to add more points."
});
alertPopup.then(function(res) {
console.log("You already created this airline! Go to the 'Add Ticket' page to add more points.");
});
}
})
}
}
}])
Thanks for the help and I can provide more code or screenshots if needed.
The issue is in this piece of code:
theUser.child("miles").child($scope.selectedAir.name.$id).once("value", function(snapshot) {
$timout(function() {
var exist = snapshot.exists();
// Check if object id exists, if so notify user
if(!exist) {
// Save and update program to user object
selected[$scope.selectedAir.name.$id] = $scope.miles.num;
theUser.child("miles").update(selected);
//$scope.hide();
$state.go("app.saved");
} else {
// Popup alert
var alertPopup = $ionicPopup.alert({
title: 'Oops!',
template: "You already created this airline! Go to the 'Add Ticket' page to add more points."
});
alertPopup.then(function(res) {
console.log("You already created this airline! Go to the 'Add Ticket' page to add more points.");
});
}
});
})
When you call once(), it starts loading data from Firebase. Since this may take some time, you pass in a callback function that is invoked when the data is available. But by the time the callback function is invoked, AngularJS is not expecting updates to the $scope anymore.
The solution is to wrap the code into a $timeout(), which ensures it gets executed when AngularJS is ready to handle scope changes again:
theUser.child("miles").child($scope.selectedAir.name.$id).once("value", function(snapshot) {
// Update scopes
var exist = snapshot.exists();
// Check if object id exists, if so notify user
if(!exist) {
// Save and update program to user object
selected[$scope.selectedAir.name.$id] = $scope.miles.num;
theUser.child("miles").update(selected);
//$scope.hide();
$state.go("app.saved");
} else {
// Popup alert
var alertPopup = $ionicPopup.alert({
title: 'Oops!',
template: "You already created this airline! Go to the 'Add Ticket' page to add more points."
});
alertPopup.then(function(res) {
console.log("You already created this airline! Go to the 'Add Ticket' page to add more points.");
});
}
})
Note that this problem wouldn't happen if you used AngularFire's $firebaseObject() and $firebaseArray() primitives, since those automatically notify AngularJS of scope changes.
We get this question a lot. Here's a recent one: Taking long to load
Related
I was wondering how an error alert would be implemented using angularjs.
Required functionality:
An alertQueue consists of all the alerts to be displayed to the user. These alerts are deleted from the queue after a span of 3 seconds. The user himself can close the alert by clicking the close button.
This AlertService must be the core service. Alerts are rendered in the view as <alert-list></alert-list>i.e using a component alertList.
Should be able to update alerts from other controllers like: AlertService.alert("my alert").
so far what I have done?
angular.
module('core').
factory('AlertService', [function() {
var alertQueue = [];
var addAlert = function(message, type){
message = {message: message, type: type};
alertQueue.push(message)
};
var deleteAlert = function(alert){
alertQueue.splice(alertQueue.indexOf(alert), 1);
};
return{
warning: function(msg){
addAlert(msg, "warning");
},
success: function(msg){
addAlert(msg, "success");
},
removeAlert: function(alert){
deleteAlert(alert);
},
getAlerts: function(){
return alertQueue;
}
}
}]);
angular.
module('alertApp').
component('alertList', {
templateUrl: '/static/js/app/aurora-alert/aurora-alert.template.html',
controller: ['$routeParams','$scope', 'Aurora',
function AlertController($routeParams, $scope, AlertService) {
var self = this;
self.alertQueue = AlertService.alertQueue;
self.alert = function(){
var message = arguments[0];
AlertService.warning(message);
};
self.removeAlert = function(alert) {
AlertService.removeAlert(alert);
};
}
]
});
I know that I'm doing something wrong in the above code and in its logic. I said above that I require the <alert-list></alert-list> component. So the alertService is injected as a dependency into alertController. But how am I going to raise the alert from other controllers? I know we can use $scope.$broadcast but that doesn't feel right.
Please explain how to achieve this? No third party libraries are to be used.
I think you are going about it only slightly incorrectly. Your alert-list should be responsible only for displaying and removing alerts, not for creating them. Leave the creation of alerts to your controllers
So for example, if you run into an error with an ApiSerivce:
DemoCtrl(AlertService, ApiService) {
ApiService.submitForm({some:data}).then(function() {
//something successfull happened
}).catch(function(error) {
AlertService.warning("Something bad happened calling the API serivce");
});
}
Then you can change your AlertService to broadcast an event when a new alert is created that the alert-list can listen to:
factory('AlertService', ["$rootScope", function($rootScope) {
var alertQueue = [];
var addAlert = function(message, type){
message = {message: message, type: type};
alertQueue.push(message)
$rootScope.$broadcast("new-alert"); //notify the list that there are new alerts
};
This is how you would listen to it in your alert-list:
$scope.$on("new-alert", function() {
self.alertQueue = AlertService.alertQueue;
});
This way, as soon as an alert is created, the alert-list is instantly updated with the latest queue of alerts.
You would probably want to do the same thing for alert deletion.
This is a controller where I am getting some data from firebase and updating my scope, then I want to do some more stuff once that scope is updated:
.controller('loginCtrl', ['$scope','$firebaseAuth','$firebaseObject','$location',
'localStorageService','$q',
function($scope,$firebaseAuth,$firebaseObject,$location,
localStorageService,$q) {
// Called
$scope.googleAuth = function(){
var auth = $firebaseAuth();
auth.$signInWithPopup("google").then(function(firebaseUser){
// Google approved
// Step 1. Get user profile
getProfile(firebaseUser);
}).catch(function(error){
console.log("Authentication failed:",error);
$scope.errorMsg = "Something went wrong. Please try again."
});
}
// Watchers
$scope.$on('profile',function(){
console.info("REACH");
if($scope.profile == null){ // User doesn't exist
console.info("profile null");
if(isAssigned(firebaseUser) != null){
// Redirect to create a profile page
} else {
// Do not let them in TODO: adjust the text
$scope.erorrMsg = "It seems you don't have a profile. Please contact us on ----------- to get a profile";
}
} else {
// Step 2. Get user parent info
// getParent($scope.profile.current_centre_id,$scope.profile.refer_id);
console.info("Getting parent");
getParent('-KeVj_sr-uCY3zx0kbb6','-KeVpUA4OIK4msNIl0gQ');
}
});
// After getting parent info
$scope.$on('parent',function(){
localStorageService.set('currentUser',{
profile: $scope.profile,
parent: $scope.parent
});
});
var getProfile = function(firebaseUser){
var email = firebaseUser.user.email;
var ref = firebase.database().ref();
// Getting profile
$scope.profile = $firebaseObject(ref.child('profiles').orderByChild('email').equalTo('dev.beezbutt#gmail.com'));
}
var getParent = function(centre_id,parent_id){
$scope.parent = $firebaseObject(ref.child('parents/'+centre_id+'/'+parent_id));
}
var isAssigned = function(firebaseUser){
// TODO
return false;
}
}]);
$scope.$on('profile') & $scope.$on('parent') are not being triggered, and I'm not sure why or what am I missing.
I know that $scope.profile is being set because I'm printing it in html.
Instead of $scope.$on, use $scope.$watch to watch the scope variables.
$scope.$watch("scope_variable_name", function(newVal, oldVal){
// Add your code here
});
I build chat function in my web app and i am about to create chat functionality between logged clients. Here is my screen from application to show exactly what i want to solve
Screen of my app
As you can see i got list of online users stored in scope in sidebar. Its created as partial view in my Asp.Net with .cshtml and i render content in "white box" using angular routing.
Problem is i use same controller twice and it creates new scope for each html so i got data in my sidebar, but in my content view i dont have any data. I am thinking about passing my data to rootscope, but i dont know if its good idea.
So my question is. Is there anything how i can clone my data from one controller to another or how i can solve this without changing functionality and if i can keep my views controlled with one controller.
Here is my PrivateChatController.js
(function () {
'use strict';
app.controller('PrivateChatController', ['$rootScope', '$scope', 'SignalRService', '$location', 'PrivateChatService', PrivateChatController]);
function PrivateChatController($rootScope, $scope, SignalRService, $location, PrivateChatService) {
//angular stuff
$scope.online_users = [];
$scope.isChatHidden = false;
$scope.openPrivateChatWindow = function (index) {
// $scope.isChatHidden = true;
angular.forEach($scope.online_users, function (value, key) {
if (index == key) {
$rootScope.currentPrivateChatUser = ({
UserName: value.UserName,
ConnectionId: value.connectionId,
});
$location.path("/details/" + value.UserName);
}
});
};
$scope.closePrivateChatWindow = function (index) {
$scope.isChatHidden = false
};
//signalR stuff
var chatHub = $.connection.chatHub;
$.connection.hub.logging = true;
chatHub.client.foo = function () { };
registerClientMethods(chatHub);
$.connection.hub.start()
.done(function(){ console.log('Now connected, connection ID=' + $.connection.hub.id); })
.fail(function () { console.log('Could not Connect!'); });
function registerClientMethods(chatHub) {
//user object
chatHub.client.newOnlineUser = function (user) {
var newUser = ({
connectionId: user.ConnectionId,
UserName: user.UserName
});
$scope.online_users.push(newUser);
$scope.$apply();
};
//compare scope online users with server list of online users
chatHub.client.getOnlineUsers = function (onlineUsers) {
//loop through scope
angular.forEach($scope.online_users, function (scopeValue, scopeKey) {
//loop through received list of online users from server
angular.forEach(onlineUsers, function (serverListValue, serverListKey) {
if (!(serverListValue.ConnectionId == scopeValue.connectionId)) {
var newUser = ({
connectionId: serverListValue.ConnectionId,
UserName: serverListValue.UserName
});
$scope.online_users.push(newUser);
$scope.$apply();
}
})
})
};
chatHub.client.onUserDisconnected = function (id, user) {
var index = 0;
//find out index of user
angular.forEach($scope.online_users, function (value, key) {
if (value.connectionId == id) {
index = key;
}
})
$scope.online_users.splice(index, 1);
$scope.$apply();
};
}};})();
Consider using services as a layer for data sharing. It should also contain chat related logic, in my opinion controllers should be as thin as possible.
Move chatHub.client.getOnlineUsers function to the service and create getter for users.
Further read
I am developping a web app with Ionic 1 and AngularJS 1.
In my factory (UserFact) :
.factory('UserFact', function() {
var user = [];
return {
'setUser': function(user) {
this.user = user;
console.log('(2) User set: ' + this.user);
console.log('(3) User id is now: ' + this.user.uid);
},
'updateSport': function(sportid, registered) {
console.log('Update sport: ' + sportid + ' --> ' + registered);
console.log('(4) For user uid: ' + this.user.uid);
var ref = firebase.database().ref('users/' + this.user.uid + '/sports/');
// sync down from server
var list = [];
ref.on('value', function(snap) { list = snap.val(); });
if(registered) {
list.splice(0, 0, {id: sportid});
} else {
}
ref.set(list);
}
};
})
In my controller :
function ($scope, $stateParams, $state, DatabaseFact, UserFact) {
// variables
$scope.sports = [];
$scope.sports = DatabaseFact.getSports();
// functions
$scope.updateSport = UserFact.updateSport;
// execution
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
UserFact.setUser(user);
console.log('(1) Ctrl user uid: ' + user.uid);
}
});
}
According to the console: logs (1), (2) and (3) display a userid form my db but (4) is always undefined...
Any idea?
Thanks
UPDATE:
Manuel, sorry, I think I had missed the point of your question. You are correct, using a factory/service, is the correct way to store the state of your application. From the above, I do not see a reason for your code not work. The user must be getting re-assigned elsewhere for you to be seeing undefined in (4), if you are not seeing the same in (3). For simplicity sake, I removed references to firebase and created a working demo: https://plnkr.co/edit/vaN7ySche8GgRQmZgFsa?p=preview
While the demo may not solve your problem, I hope it illustrates that the factory variable (user) is persisted in memory and usable across multiple factory method calls.
ORIGINAL ANSWER (MISSED THE POINT) BELOW:
Update the state change handler to save the user on the controller scope:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
$scope.user = user;
UserFact.setUser(user);
console.log('(1) Ctrl user uid: ' + user.uid);
}
});
Then, in the template, invoke the updateSport method, using the user scope variable:
updateSport(user, true);
or
updateSport(user, false);
You need to pass the arguments while calling updateSport in controller,
$scope.updateSport = UserFact.updateSport(userid,registered);
I am getting an error retrieving current user id from my factory/service. Removing this firebase.auth().currentUser.uid seems to work.Am I missing something?Is it possible to get user id from factory?
.factory("dataFire", ["$firebaseArray", "$firebaseAuth", "$firebaseObject", function($firebaseArray, $firebaseAuth, $firebaseObject){
var ref = firebase.database().ref("users/" + firebase.auth().currentUser.uid);
var user = $firebaseObject(ref);
return {
user: user
} }])
jsfiddle link
https://jsfiddle.net/ken19net/m8qkdrpu/37/
Your code that attach the listener to the database, runs before the user is signed in. Since there is no current user at that point firebase.auth().currentUser.uid will throw the following error:
VM603 angular.min.js:118 TypeError: Cannot read property 'uid' of null
In future questions please include the error message, as it would've saved us both some time.
The solution is to attach the listener after the user has been authenticated:
app.controller("appCtrl", function($scope, $firebaseAuth, myService) {
$scope.signIn = function() {
$firebaseAuth().$signInAnonymously().then(function(user) {
var ref = firebase.database().ref("test/" + firebase.auth().currentUser.uid);
});
};
});
Note that running your code like above will create a new anonymous user account every time the user loads the page. If that is not what you want, use an auth state listener:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
var ref = firebase.database().ref("test/" + firebase.auth().currentUser.uid);
}
});
See the documentation on getting the currently signed in user.
I might of had a similar problem. This might help.
if (firebase.auth().currentUser !== null) {
console.log("user id: " + firebase.auth().currentUser.uid);
var ref = firebase.database().ref().child("users/" + firebase.auth().currentUser.uid);
var syncUserObject = $firebaseObject(ref);
syncUserObject.$bindTo($scope, "data");
} else {
console.log("No user currently logged in");
}
if all else fails then just JSON.stringify(Object ,null, 2);