I have a create function in a service which is responsible for taking input data, turning it into a resource, doing a save and inserting it into a local cache.
create = (data, success, error) ->
throw new Error("Unable to create new user, no company id exists") unless $resource_companies.id()
# create the resource
new_model = new resource(data)
# manually add the company id into the payload (TODO: company id should be inferred from the URL)
new_model.company_id = $resource_companies.id()
# do the save and callbacks
new_model.$save( () ->
list.push(new_model)
success.call(#) if success?
, error)
My problem is that an ng:repeat that watches the list variable is not getting updated. As far as I can tell this is not occurring outside on AngularJS so it does not require a $scope.$apply() to keep it up to date (indeed, if I try to trigger a digest, I get a digest already in progress error)
What's doubly weird, is I have this exact same pattern used elsewhere without issue.
The code used by my controller to access the list array is (this is in the service)
# refreshes the users in the system
refresh = (cb) -> list = resource.query( () -> cb.call(#) if cb? )
In the controller:
$scope.users = $resource_users.refresh()
This may be because you are assigning a new reference. Angular is watching the old reference, and it never changes. Here is the where the reference is replaced:
list = resource.query( /* ... */ )
If, instead, you make the list a property of an object, angular's ng-repeat watch should be able to observe the change in the list property of the data object:
data.list = resource.query( /* ... */ )
Related
I am working on a website which uses Angular (1.6.4) Select. The content for the select element is loaded via REST if it is requested the first time. But the response of the REST call is cached so that following calls load the data from memory. If I load the website the select element is empty and I can't select any value. If I visit the site again with the data cached, the selectbox allows you to select items from a list. If I redirect the REST call to a file containing the data it works on the first attempt and I can select items as expected
So it seems that the code works in principle but if the model is updated too late the select element does not notice the changes.
Here is the code I am using:
<select ng-model="myData" chosen
class="select--chosen"
ng-change="handleSelection()"
ng-options="myData as myData.value for myData in dataArray">
</select>
The controller code looks like this (called when site is opened):
$scope.dataArray = [];
//$scope.dataArray = [{value : "just a test value"}];
$scope.$watch('dataArray ', function() {console.log("dataArray was changed...")}, true);
getArray();
function getArray() {
DataFactory.getArray().then(function (data) {
$scope.dataArray = data;
});
}
I do get the watch message when I load the site for the first time. When looking for a solution I found several hints but none of them worked for me. This is what I tried:
1) Add
$scope.$apply(function(){ /* code */ });
to set the dataArray inside this function or call it inside of the watch-function. In both cases I got the error on the console that the digest is already updating or so, indicating that it is not neccessary to use scope.$apply
2) Use
$scope.onChange($scope.dataArray);
after setting dataArray = data.
Unfortunately nothing worked. If I uncomment the line:
$scope.dataArray = [{value : "just a test value"}];
I can choose this entry after loading the page and the select view then shows the first entry of the dataArray and afterwards I can access the whole list and select items from it.
So I would like to know what I can do to update the select view after the data is available. Either by adding a Listener or by manually calling the select view to update(), refesh() or so. Is there such a function available?
You can show your select element by some boolean flag, which sets true, when
data loaded.
You can do something like below code.
In controller :
$scope.dataArray = [];
$scope.myData= null;
$scope.isDataLoaded = false; //flag for data loading.
function getArray() {
DataFactory.getArray().then(function (data) {
$scope.isDataLoaded = true; // make true now
$scope.dataArray = data.data; //your result might be data.data
$scope.myData = $scope.dataArray[0]; // you may select 1st as default
});
}
getArray();
In html:
<select ng-if="isDataLoaded" ng-model="myData" ng-class="select-chosen"
ng-options="myData as myData.value for myData in dataArray">
</select>
According to the Angularfire docs, when working with an object returned through $asObject(), you can set priority for said object by defining a $priority property on the object and then using $save().
My code works great, but $priority isn't doing anything. Here's some code with complete explanations in the comments:
app.factory('MyService', function($rootScope, $firebase) {
// The complete Firebase url
var ref = *obfuscated*;
// Returning the dataset as an object containing objects
var data = $firebase(ref).$asObject;
// This object is what's returned by MyService
var Data = {
// Method to create a new object within the data set,
// keyed by myId. Attempting to set priority for the
// record via $priority. returnData.uid is a valid string.
create: function(returnData, myId) {
data[myId] = {
myId: myId,
$priority: returnData.uid
};
// No need to explain the stuff between here and the
// $rootScope listener below, just added for context
data.$save().then(function() {
setMyId(myId);
});
},
findByMyId: function(myId) {
if (myId) {
return data[myId];
}
}
};
function setMyId(myId) {
$rootScope.myId = User.findByMyId(myId);
}
// This event listener works fine, fires
// at user login and returns data
$rootScope.$on('$firebaseSimpleLogin:login', function(e, returnData) {
// returnData.uid has the correct value - this
// function should return the object(aka record) with
// a priority matching returnData.uid
var query = $firebase(ref.startAt(returnData.uid).endAt(returnData.uid)).$asObject();
// console shows an object with the normal $firebase
// properties, but no records. If I define query without
// limiting the set ($firebase(ref).$asObject()), it returns
// the full set just fine. The priority on all records is still
// null.
console.log(query);
query.$loaded(function() {
setData(query.myId);
});
});
return Data;
});
Yes, I'm following Thinkster.io's tutorial and I'm in Chapter 7. No, this is not a duplicate of the other questions about that chapter, I already found my way around the pre-Angularfire 0.8 code present in their examples, just can't set $priority, and I've spent about 5 hours so far trying to find a solution through my own efforts and on the web.
Any takers?
When viewed in the light of how JavaScript works with objects (i.e. unordered), how JSON handles objects (i.e. unordered), and in light of the expectation that AngularFire's $asObject() method is intended for storing key/value pairs, and singular records that are not used as a collection, this starts to make some sense.
Internally, the synchronize'd object's $save method calls Firebase's setWithPriority. In set or setWithPriority calls, the child nodes are replaced. Any meta data like priorities on those children are replaced.
In AngularFire, $asArray is intended to handle ordered collections, and provides the ability to set $priority on child nodes (only one level deep, of course, as it treats its children as singular records that are not themselves collections).
Since, in your case, you want to work with fixed keys rather than push ids, you'll probably want to override the $add method using $extendFactory and do something like the following:
angular.module(MY_APP).factory('FixedKeysFactory', function($FirebaseArray, $firebaseUtils) {
return $FirebaseArray.$extendFactory({
$add: function(data) {
this._assertNotDestroyed('$add');
if( angular.isObject(data) && typeof data.$id === 'string' ) {
return this.$inst().$set(data.$id, $firebaseUtils.toJSON(data));
}
else {
return this.$inst().$push($firebaseUtils.toJSON(data));
}
}
});
});
You could then pass this into your $firebase instance in place of the default factory:
var list = $firebase(ref, {arrayFactory: FixedKeysFactory}).$asArray();
A simpler but less awesomatic™ solution would be to manually add your objects to the array, manually giving them a $id, then call $save:
var list = $firebase(ref).$asArray();
var i = list.length;
list.push({ foo: 'bar', $id: 'kato' });
list.$save(i);
Some notes on the future: It will soon be possible to use any field as sort criteria and there will be no need to set priorities (yay!). It will probably be possible to set your own $id before calling $add on a synchronized array in AngularFire as soon as I clear that with the other devs (like the 0.8.3 release).
My application requires candidates to be set up with an agency and nationality (among other things). I want to set the list of agencies and nationalities in global variables so that they can be accessed by the profile screen rather than retrieved from the database every time (as they are currently). Thanks to the other questions here I've got so far... but a crucial puzzle piece is clearly missing. The data is being retrieved from the DB but then isn't visible elsewhere.
homecontroller.js which loads up the data...
//Home page Controller
function HomeCtrl($scope, $rootScope, SessionTimeoutService, GetAllAgencies, GetNationalityList){
GetAllAgencies.getData({}, function(agencieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$rootScope.agencieslistglobal = agencieslist.data;
});
/* I tried hard-coding values here - that worked & was passed thro' ok
$rootScope.nationalitieslistglobal = [
{'nationality_id' : 0, 'name' : 'Unknown'},
{'nationality_id' : 1, 'name' : 'Known'},
{'nationality_id' : 2, 'name' : 'Pants'},
]; */
GetNationalityList.getData({}, function(nationalitieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$rootScope.nationalitieslistglobal = nationalitieslist.data;
});
alert($rootScope.nationalitieslistglobal[8].name);
}
The alert doesn't fire at all.
From the candidatescontroller.js:
function CandidatesAddCtrl($scope, CandidateModel, GetNationalityList, SessionTimeoutService, $http, GetAllAgencies, $rootScope) {
/* commented out as this should now be done globally - this works when it's in place
GetAllAgencies.getData({}, function(agencieslist) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$scope.agencieslist = agencieslist.data;
});
*/
CandidateModel.getBlankCandidate();
$scope.candidateinfo = CandidateModel;
// get global agencies list
$scope.agencieslist = $rootScope.agencieslistglobal;
alert($scope.agencieslist); // shows 'undefined'
/*
GetNationalityList.getData({}, function(nationalitieslist) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$scope.nationalitieslist = nationalitieslist.data;
});
*/
// get global nationalities list
$scope.nationalitieslist = $rootScope.nationalitieslistglobal;
....
}
So... when I hard-code the data outside of the GetNationalityList.getData function (the bit that's commented out in the example above), it is populated & passed through OK & my drop-down list populates. When I don't do that, the $rootScope values are 'undefined'.
I have 2 theories -
somehow the homecontroller $rootScope isn't being recognised as the global I'm intending (that's just the name of the variable) and some other "passing back" action needs to be taken (I've tried several variations on "return nationalitieslist.data" and assigning it to other variables/handles). Also, nationalitieslist.data is in the same format as the hard-coded list, only it's longer.
I'm being caught out by the asynchronous nature of javascript and when I load the second page, the data just isn't there yet. I'm not convinced this is right as the DB call is finishing and I had an alert which showed me a random nationality name and it was there.
My frustration in part is coming from the fact that the GetNationalityList and GetAllAgencies code snippets work and assigns values correctly in the candidate controller (the bits that are now commented out), but are working subtly differently in the homecontroller.
Top tips, anyone, please?
I am sure this article can help.
http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
I had somewhat of a similar issue but I needed to emit from a factory.
My problem was answered here.
Angular $rootScope.$on Undefined
Your 2nd point is more your issue, although it's not JavaScript that's async in nature, it's Angular services such as your GetAllAgencies service. You're making async calls but not waiting for the response. That's why when you assign it manually it all works. Try putting your alert() inside the callback function to the getData() call. This should highlight how the process should work.
For clarity when I say invalid references I mean something like:
https://[appname].firebaseIO.com/assets/:id
Where the ID doesn't match an item in firebase
Previously I have been doing this by looking at the item in the scope and checking if it's empty but this new version of AngularFire attaches all its methods ($add, $bind, $child etc) even when there is no data to load so checking if the object in the scope is empty no longer works.
Is there a more correct way to do this or should I perhaps check for items in the object that aren't functions as an alternative way to do this?
In 0.5, the loaded event returns the raw value, which should suit your needs here:
var fbRef = new Firebase(URL);
$firebase( fbRef ).$on('loaded', function(value) {
if( value === null ) { console.log('no record at this path'); }
});
I created my web site having 2 types of users: admin and user. So, I created 3 pages mainpag.html, admin.html, user.html. and separate models, views, collections, routers.js files for each of them. After logging in, as I am sending users to separate HTML pages with different models, I can't automatically get user model. so I did like this:
First, I made AJAX call to server, asking for the _id (username in session, so I can get id)
from the id, I fetched the model, by model.fetch(), then I got my usermodel with all attributes.
then in the success callback of fetch, I did model.save({weight: "somevalue"}). According to me, it should update right, as the model is already available, that attribute weight also available with some old value, but it is sending POST request, also when I tried model.isNew(), it returned true. Where am I wrong? how can I update my model? I will post more details if required.
More details:
If I remove that save method, then I am getting correct attributes in the model.
If I don't remove that save method, that success and error callbacks are also appearing as attributes in the model.
Code:
addWeight : (e)->
arr=new Array()
arr['_id']=app._id
console.log "asdasd"
console.log arr
console.log arr['_id']
#user_model =new UserModel(arr)
#user_model.fetch({
success : (model,res,options) =>
console.log model
console.log res
arr=new Array()
arr['_id']=e.target.id
#arr['action']='weight' #means , update weight
##user_model.setArr(arr)
##user_model.set({weight : arr['_id']})
console.log "new : "+#user_model.isNew()
#user_model.save({weight : e.target.id})
##user_model.save({
# success : (model,res,options) =>
# console.log "model updated: "+JSON.stringify(model)
# console.log "Res : "+JSON.stringify(res)
# error : (model,res,options) =>
# console.log "Error : "+JSON.stringify(res)
#})
error : (model,res,options) =>
console.log "Error "
})
the above code is written in coffeescript, so even if you don't know coffeescript, don't worry, you can understand easily, and those # mean, it is a comment. here we follow indentation instead of braces.
one more doubt, a model's URL must be changed dynamically according to the requirement, right? what is the best way to achieve that? I am doing like this:
I am populating "array" containing the required fields that should be present in the URL. In model, s init func, I am using #arr=arr, then in URLs function, I check like this.
url : ->
if #arr['id']
"/user/#{#id}"
Is my approach right, or any better approach is there for dynamically setting URLs. Or can I directly set the URLs like this:
#user_model.setUrl "/someurl/someid" //this setUrl method is available in model's definition
#user_model.fetch() or save() or watever that needs url
Just a hunch, but you mentioned that you call model.fetch() to retrieve the _id field. Be sure to either return an id field instead _id (notice the underscore).
The call to model.isNew() returning true is an indicator that the id property was never set from the model.fetch() call.
I look forward to a possible further explanation with your code...
Looking at your code:
/* The model needs an 'id' attribute in order to marked as not new */
#user_model = new UserModel(id: arr['_id'])
Actually if you call
model.set({weight: "somevalue"});
It will update the value in the model, but it won't send a POST request
model.save(attribute);
Actually calls Backbone.sync as you probably know.
EDIT :
You might want ot set
m = Backbone.Model.extend({
idAttribute: '_id'
});
to every model, because the isNew method actually checks if the model has id attribute
Regarding to this you could see here that .set doesn't call backbone.sync here : http://jsfiddle.net/5M9HH/1/