I am building a web application just as a test now. Right now the main features are login and posting a message.
When the user presses a button, the following code is executed:
$scope.getData = function(){
$http.get('/messages').
then(function(response) {
if(angular.toJson(response.data.messages) != angular.toJson($scope.messages)) {
$scope.messages = response.data.messages;
$scope.clearAllSelected();
$scope.confirmRestOperation(response);
}
}, function(response) {
$scope.messages = [];
$scope.confirmRestOperation(response);
});
};
The HTML template dependent off of my model is as follows:
<div id="messages">
<md-card ng-repeat="message in messages" class="padding-medium box-sizing" ng-init="setAuthorDataFromAuthorId(message.author)">
<md-checkbox class="md-primary" ng-click="toggle(message.id, selected)" ng-model="exists(message.id, selected)"></md-checkbox>
<h4><b>Author: {{authorData[message.author].name}} ({{authorData[message.author].username}})</b></h4>
<img style="width:50px;height:50px;" ng-src="{{authorData[message.author].profile_picture}}" alt="(Picture)">
<p>Message: {{message.message}}</p>
<p>{{message.time | timestampToDate}}</p>
</md-card>
</div>
So the goal here is, the message object will contain author field which contains the ID of the author. On each card initialization I call a $scope function called setAuthorDataFromAuthorId, this takes in an integer value, the ID of the author who wrote the post. The following code is this:
$scope.setAuthorDataFromAuthorId = function(id) { // line 22
console.log($scope.authorData); // line 23
if(id in $scope.authorData) return;
console.log("setAuthorFromAuthorId has been called");
$http.get('/getUserDataFromId/'+id).then(function(response) {
console.log(response.data); // line 27
console.log($scope.authorData); // line 28
$scope.authorData[id] = response.data;
}, function(response) {
console.log(response.data);
console.log($scope.authorData);
$scope.authorData = {};
});
console.log("setAuthorFromAuthorId has been completed");
};
See my goal here is say there are 50 messages on the screen (obviously not realistic in any production scenario to load every single message in the database, but for a test site this is what it's doing), I don't want it to access the api endpoint /getUserDataFromId/1 50 different times if the same message is from the same author. My goal here is to see if the id is already a key in our $scope.authorData object. If so, we return a save ourselves a trip to the endpoint and loading and writing information. So in a perfect world, all 50 messages would load and in the console we would see only this:
Object {}
setAuthorFromAuthorId has been called
setAuthorFromAuthorId has been completed
Object {id: author data here}
This would happen because it would load the author information one time, and the remaining 49 it would see that the id is a key in the authorData object and it would not get past the first line in the above JS function. However here are the results:
Object {} MessageController.js:23
setAuthorFromAuthorId has been called MessageController.js:25
setAuthorFromAuthorId has been completed MessageController.js:35
Object {} MessageController.js:23
setAuthorFromAuthorId has been called MessageController.js:25
setAuthorFromAuthorId has been completed MessageController.js:35
Object {full author data here from api response} MessageController.js:27
Object {} MessageController.js:28
Object {full author data here from api response} MessageController.js:27
Object {1: Object} (correct $scope.authorData) MessageController.js:28
I do not understand this output at all. It is as if the ng-init is only calling the couple lines of the function, and deferring the rest until later to actually work and update the model. I did some research and tried adding some $scope.$watch()'s and $scope.$apply()'s and digests after certain key lines of code to try and get the model to update properly but it keeps running the first console.log before the conditional as many times as there are messages, and then later it skips that part, and runs all the $http.get() as many times as there are messages, and undermines my want to save ourselves from reaching the API endpoint too many useless times.
After some thinking I decided to change the ng-init to ng-click and see if this worked at all. It was perfect. If there were 50 message all from the same user and I clicked on one, they all updated(because the model was properly updated) and if I clicked any more times after that the API would not be reached any more because the function is working how it lexically should be working. Here was the output after changing the ng-init to ng-click:
Object {} MessageController.js:23
setAuthorFromAuthorId has been called MessageController.js:25
setAuthorFromAuthorId has been completed MessageController.js:35
Object {full author data from API} MessageController.js:27
Object {} MessageController.js:28 // this is the authorData before it was updated with the above data
Object {1: Object} MessageController.js:23
Object {1: Object} MessageController.js:23
Object {1: Object} MessageController.js:23
Object {1: Object} MessageController.js:23 // this would happen as many times as I clicked, which is the correct response
Any recommendations as to how I can get this to work properly and as expected (to me at least) with ng-init or at least some reasoning as to why it is failing with ng-init?
My gut tells me perhaps I cam trying to read or write something a value before it has been written, this would warrant a $scope.$watch() however I have tried it (perhaps not correctly though!).
Thanks
Forget ng-init, move the initialization inside the controller.
Chain from the first $http request.
When the user presses a button, execute the following code:
$scope.getDataAndInitPromise = function() {
$scope.pendingFlags = $scope.pendingFlags || {};
var promises = [];
//compute getDataPromise
var getDataPromise = $scope.getDataPromise();
//chain from getDataPromise
var initListPromise = getDataPromise.then (function() {
angular.forEach($scope.messages, function(m) {
var id = m.author;
if ($scope.pendingFlags[id] == "pending") return;
if(id in $scope.authorData) return;
console.log("setAuthorFromAuthorId has been called");
//set pending flag
$scope.pendingFlags[id] = "pending";
var p = $http.get('/getUserDataFromId/'+id
).then(function(response) {
console.log($scope.authorData);
$scope.authorData[id] = response.data;
}).catch(function(error) {
//log error
}).finally ( function() {
console.log("setAuthorId %s done ", id);
//clear pending flag
$scope.pendingFlags[id] = "done";
});
//push out promises
promises.push(p);
});
//return promises array for further chaining
return promises;
});
//return for further chaining
return initListPromise;
}
Notice the use of pendingFlags to prevent a repeat fetch if a fetch is already in process. Also notice that I moved the console.log report of "done" into a .finally method.
Of course modify, getData to return a promise for chaining.
$scope.getDataPromise = function(){
var getDataPromise =
$http.get('/messages').
then(function(response) {
if(angular.toJson(response.data.messages) != angular.toJson($scope.messages)) {
$scope.messages = response.data.messages;
$scope.clearAllSelected();
$scope.confirmRestOperation(response);
}
}, function(response) {
$scope.messages = [];
$scope.confirmRestOperation(response);
});
//return promise for chaining
return getDataPromise;
};
Leverage the power of promises.
Don't throw away those httpPromises; use them.
Related
I am new to Angular, but managed to make an Ajax-call and print out users from Random User Generator API in a list view.
Now I want to make a detailed view while clicked on a user.
In my HTML I make a function call: fetchInfoById(user.id.value)
In my script the function:
$scope.fetchInfoById = function(info_id) {
$http.get("https://randomuser.me/api/?id.value="+info_id)
//also tried: $http.get("https://randomuser.me/api/?id/value="+info_id)
.success(function(data) {
$scope.oneUserResult = data.results;
});
}
It does give me a user to a detail view, but not the chosen one. What am I doing wrong?
Thanks for your good suggestions.
I know it is a random generator, but setting parameters for the request to: "seed=...", the same persons is displayed on each listview request:
$http.get('https://randomuser.me/api/?results=15&seed=abc&inc=gender,name,location,email,dob,phone,cell,id,picture,info,nat&nat=gb')
.success(function(response){
$scope.userResult = response.results;
});
Then I fetched the id for each person and passed in as a parameter to the function call for the request for the detail view.
I tried with console.log() to make sure I passed in the right value for the detail view request and then even hardcoded the
parameter for the request ie:
$scope.getInfoById = function(info_id) {
console.log("from HTML: "+info_id.value ); // = JK 00 46 67
$http.get("https://randomuser.me/api/?id="+'JK 00 46 67 H') ...
The jason data behind the API is formatted like this for the id-property:
{
"results": [
{
"id": {
"name": "BSN",
"value": "04242023"
},...
I still haven't figured out how to get the one user by id. Getting different users all the time, even with hard coded id...
Instead of making the second request my solution was to a pass the "clicked user" as a parameter for the detailed view.
Change your code to this:
$scope.fetchInfoById = function(info_id) {
$http.get("https://randomuser.me/api/?id="+info_id)
//also tried: $http.get("https://randomuser.me/api/?id/value="+info_id)
.success(function(data) {
$scope.oneUserResult = data.results;
});
}
Also, make sure you are passing in the correct value to this function.
Fetch a list of users from API call "https://randomuser.me/api/?results=5".
$scope.getAllUsers= function(resultCount) {
$http.get("https://randomuser.me/api/?results="+resultCount)
.success(function(data) {
$scope.users= data.results;
});
Display them on the screen.
On click of one record fetch details for that particular record from users list fetched earlier.
$scope.getUserById= function(userId) {
return $scope.users.filter(function(user) {
return user.id.value=== userId;
})[0]; // apply necessary null / undefined checks wherever required.
}
another way using ng-model:
$scope.user = {};
$scope.fetchInfoById = function() {
$http.get("https://randomuser.me/api/?id="$scope.user.id)
.success(function(data) {
$scope.oneUserResult = data.results;
});
}
The ultimate goal is to detect changes between an existing Parse object and the incoming update using the beforeSave function in Cloud Code.
From the Cloud Code log available through parse.com, one can see the input to beforeSave contains a field called original and another one called update.
Cloud Code log:
Input: {"original": { ... }, "update":{...}
I wonder if, and how, we can access the original field in order to detect changing fields before saving.
Note that I've already tried several approaches for solving this without success:
using (object).changedAttributes()
using (object).previousAttributes()
fetching the existing object, before updating it with the new data
Note on request.object.changedAttributes():
returns false when using in beforeSave and afterSave -- see below for more details:
Log for before_save -- summarised for readability:
Input: { original: {units: '10'}, update: {units: '11'} }
Result: Update changed to { units: '11' }
[timestamp] false <--- console.log(request.object.changedAttributes())
Log for corresponding after_save:
[timestamp] false <--- console.log(request.object.changedAttributes())
There is a problem with changedAttributes(). It seems to answer false all the time -- or at least in beforeSave, where it would reasonably be needed. (See here, as well as other similar posts)
Here's a general purpose work-around to do what changedAttributes ought to do.
// use underscore for _.map() since its great to have underscore anyway
// or use JS map if you prefer...
var _ = require('underscore');
function changesOn(object, klass) {
var query = new Parse.Query(klass);
return query.get(object.id).then(function(savedObject) {
return _.map(object.dirtyKeys(), function(key) {
return { oldValue: savedObject.get(key), newValue: object.get(key) }
});
});
}
// my mre beforeSave looks like this
Parse.Cloud.beforeSave("Dummy", function(request, response) {
var object = request.object;
var changedAttributes = object.changedAttributes();
console.log("changed attributes = " + JSON.stringify(changedAttributes)); // null indeed!
changesOn(object, "Dummy").then(function(changes) {
console.log("DIY changed attributes = " + JSON.stringify(changes));
response.success();
}, function(error) {
response.error(error);
});
});
When I change someAttribute (a number column on a Dummy instance) from 32 to 1222 via client code or data browser, the log shows this:
I2015-06-30T20:22:39.886Z]changed attributes = false
I2015-06-30T20:22:39.988Z]DIY changed attributes =
[{"oldValue":32,"newValue":1222}]
I am developing my app, and one of the features will be messaging within the application. What I did, is I've developed 'send message' window, where user can send message to other user. The logic behind it is as following:
1. User A sends message to User B.
2. Firebase creates following nodes in 'Messaging':
"Messaging"->"User A"->"User B"->"Date & Time"->"UserA: Message"
"Messaging"->"User B"->"User A"->"Date & Time"->"UserA: Message"
Here is the code that I am using for sending messages:
sendMsg: function(receiver, content) {
var user = Auth.getUser();
var sender = user.facebook.id;
var receiverId = receiver;
var receiverRef = $firebase(XXX.firebase.child("Messaging").child(receiverId).child(sender).child(Date()));
var senderRef = $firebase(XXX.firebase.child("Messaging").child(sender).child(receiverId).child(Date()));
receiverRef.$set(sender,content);
senderRef.$set(sender,content);
},
(picture 1 in imgur album)
At the moment, I am trying to read the messages from the database, and sort them in according to date. What I've accomplished so far, is that I have stored the content of "Messaging/UserA/" in form of an Object. The object could be seen in the picture I've attached (picture 2).
http://imgur.com/a/3zQ0o
Code for data receiving:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
My question is: how can I read the object's messages? I would like to sort the according to the date, get the message and get the Id of user who has sent the message.
Thank you so much!
You seem to be falling for the asynchronous loading trap when you're reading the messages:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
That return statement that you have in the Messages.on("value" callback doesn't return that value to anyone.
It's often a bit easier to see what is going on, if we split the callback off into a separate function:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var messagesObj = snapshot.val();
return messagesObj;
},
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
console.log('Before adding on-value listener');
Messages.on("value", onMessagesChanged);
console.log('After adding on-value listener');
}
If you run the snippet like this, you will see that the console logs:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
This is probably not what you expected and is caused by the fact that Firebase has to retrieve the messages from its servers, which could potentially take a long time. Instead of making the user wait, the browser continues executing the code and calls your so-called callback function whenever the data is available.
In the case of Firebase your function may actually be called many times, whenever a users changes or adds a message. So the output more likely will be:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
Inside on-value listener
Inside on-value listener
...
Because the callback function is triggered asynchronously, you cannot return a value to the original function from it. The simplest way to work around this problem is to perform the update of your screens inside the callback. So say you want to log the messages, you'd do:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var i = 0;
snapshot.forEach(function(messageSnapshot) {
console.log((i++)+': '+messageSnapshot.val());
});
},
Note that this problem is the same no matter what API you use to access Firebase. But the different libraries handle it in different ways. For example: AngularFire shields you from a lot of these complexities, by notifying AngularJS of the data changes for you when it gets back.
Also see: Asynchronous access to an array in Firebase
Let me explain my issue, I am trying to populate Ember.Select directly from database.
I have these routes:
this.resource('twod', function() {
this.resource('twoduser', {
path : ':user_id'
});
});
In twoduser, I am displaying a full information about a single user. In that view, I have a Select Box as well, which end user will select and then with a button, he can add the user to a team that he selected from Ember.Select.
I tried to do this,
App.TwoduserController = Ember.ArrayController.extend({
selectedTeam : null,
team : function (){
var teams = [];
$.ajax({
type : "GET",
url : "http://pioneerdev.us/users/getTeamNames",
data : data,
success : function (data){
for (var i = 0; i < data.length; i ++){
var teamNames = data[i];
teams.push(teamNames);
}
}
});
return teams;
}.property()
})
Then in my index.html:
{{view Ember.Select
contentBinding="team"
optionValuePath="teams.team_name"
optionLabelPath="teams.team_name"
selectionBinding="selectedTeam"
prompt="Please Select a Team"}}
But when I do this, for some reason it interferes with Twoduser and I am not able to view the single user.
Furthermore, here's a sample JSON response I will get through the url:
{"teams":[{"team_name":"Toronto Maple Leafs"},{"team_name":"Vancouver Canuck"}]}
Moreover, I am fetching all users using Ajax like this:
App.Twod.reopenClass({
findAll : function() {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.getJSON("http://pioneerdev.us/users/index", function(data) {
var result = data.users.map(function(row) {
return App.Twod.create(row);
});
resolve(result);
}).fail(reject);
});
},
findBy : function(user_id) {
return new Ember.RSVP.Promise(function(resolve, reject) {
var user = App.Twod.create();
$.getJSON("http://pioneerdev.us/users/byId/" + user_id, function(data) {
var result = user.setProperties(data.user);
resolve(result);
}).fail(reject);
});
}
});
Though there's one thing, I have a separate Teams route:
this.resource('teamview', function(){
this.resource('teamviewdetail', {
path : ':team_id'
});
});
Which shows all the teams and a single team when you click on a single team.
Can I use that TeamviewController? or Can I fetch team names from Twoduser Controller and push names to the array as I mentioned before?
More Information:
If I use the way I mentioned, I get this error:
Uncaught TypeError: Object [object Object] has no method 'addArrayObserver'
Here's a working jsfiddle on the issue I am experiencing. You can select "Storyboard" from the Designation & then select the user. That will reproduce the issue.
One more Update: Seems using ObjectController instead of ArrayController issue solves the addArrayObserver issue. But still I can't get the teams in the Ember.Select.
The biggest issue here is that you use Array#push instead of pushObject. Ember needs the special methods in order to be aware of changes. Otherwise, it will continue to think that the array of teams is as empty as when you first returned it. Second biggest issue is that your ajax success call isn't accessing the returned data properly.
Also, optionValuePath and optionLabelPath are relative to the individual select option view, so they should start with content, which is the individual item as set on the view. So: content.team_name
I'm trying to grab all the URLs of my Facebook photos.
I first load the "albums" array with the album id's.
Then I loop through the albums and load the "pictures" array with the photos URLs.
(I see this in Chrome's JS debugger).
But when the code gets to the last statement ("return pictures"), "pictures" is empty.
How should I fix this?
I sense that I should use a closure, but not entirely sure how to best do that.
Thanks.
function getMyPhotos() {
FB.api('/me/albums', function(response) {
var data = response.data;
var albums = [];
var link;
var pictures = [];
// get selected albums id's
$.each(data, function(key, value) {
if ((value.name == 'Wall Photos')) {
albums.push(value.id);
}
});
console.log('albums');
console.log(albums);
// get the photos from those albums
$.each(albums, function(key, value) {
FB.api('/' + value + '/photos', function(resp) {
$.each(resp.data, function(k, val) {
link = val.images[3].source;
pictures.push(link);
});
});
});
console.log('pictures');
console.log(pictures);
return pictures;
});
}
You're thinking about your problem procedurally. However, this logic fails anytime you work with asynchronous requests. I expect what you originally tried to do looked something like this:
var pictures = getMyPhotos();
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
But, that doesn't work since the value of 'pictures' is actually undefined (which is the default return type of any function without an actual return defined -- which is what your getMyPhotos does)
Instead, you want to do something like this:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
callback(pictures);
});
}
// This is the function that actually does the work with your pictures
function oncePhotosReceived(pictures){
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
};
// Request the picture data, and give it oncePhotosReceived as a callback.
// This basically lets you say 'hey, once I get my data back, call this function'
getMyPhotos(oncePhotosReceived);
I highly recommend you scrounge around SO for more questions/answers about AJAX callbacks and asynchronous JavaScript programming.
EDIT:
If you want to keep the result of the FB api call handy for other code to use, you can set the return value onto a 'global' variable in the window:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
window.pictures = pictures;
});
}
You can now use the global variable 'pictures' (or, explicitly using window.pictures) anywhere you want. The catch, of course, being that you have to call getMyPhotos first, and wait for the response to complete before they are available. No need for localStorage.
As mentioned in the comments, asynchronous code is like Hotel California - you can check any time you like but you can never leave.
Have you noticed how the FB.api does not return a value
//This is NOT how it works:
var result = FB.api('me/albums')
but instead receives a continuation function and passes its results on to it?
FB.api('me/albums', function(result){
Turns out you need to have a similar arrangement for your getMyPhotos function:
function getMyPhotos(onPhotos){
//fetches the photos and calls onPhotos with the
// result when done
FB.api('my/pictures', function(response){
var pictures = //yada yada
onPhotos(pictures);
});
}
Of course, the continuation-passing style is contagious so you now need to call
getMyPhotos(function(pictures){
instead of
var pictures = getMyPhotos();