I have a problem with creating Ember objects from a JSON ajax data source. If I create the object the manual way, it works perfectly, and the view gets updated. If the data itself comes from a JSON ajax data call, however, it does not work. If I inspect the resulting objects, the Ember model objects does not get the correct getter and setter properties. Does anyone know why this happens?
App.AlbumView = Ember.View.extend({
templateName:'album',
albums:[],
getAll:function() {
var self = this;
//This works!
self.albums.push(App.Album.create({title: 'test', artist: 'test'}));
$.post('/Rest/list/album',null,function(data) {
$.each(data, function (index, item) {
//This does not work?!?
self.albums.push(App.Album.create(item));
});
}, 'json');
}
});
You should always use embers get('variableName') and set('variableName', newValue) methods when accessing instance variables of a view. Strange things tend to happen if you don't.
Related
In Ember, you can reference a property in a template, and the template will wait for that property to be populated before rendering.
This works great for obtaining a list of entries, which are populated by an external REST endpoint:
App.ItemsListRoute = Ember.Route.extend({
model: function() {
return {
client: App.Client.create()
}
}
});
Where the Client constructor looks like:
App.Client = Ember.Object.extend({
init: function() {
var _this = this; // For referencing in AJAX callback
$.ajax({
url: MY_API_URL,
type: 'GET'
}).done(function(res) {
_this.set('itemsList', parsedDataFromRes);
});
},
});
For my template that relies on the itemsList, this works great:
...
{{#each item in model.client.itemsList}}
<tr>
...
However, I have another route for a statistics, in which I would like to do some calculations on the results of the request, and return those values to the template:
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().get('itemsList').length;
return {
numItems: itemCount
}
})
I realize this is a contrived example - I could query the length on the template and it would work fine - but you'll have to humor me.
The issue with the above example is that the get('itemsList') is likely to return an undefined value, based on the data race of rendering the template, and the AJAX response and property setter being called.
How can I "wait" for a property to become available in my JS (not template code) so that it can be used to provide a model for the template?
Is converting the 'itemsList' property to a function returning a promise the most "Ember-Like" way of doing things? Would this heavily complicate my template logic?
You can use a promise and do additional operation in the then function call.
For example you can do
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().then(function(promiseResult){
var itemCount = promiseResult.get('itemsList').length;
return {
numItems: itemCount
}
})
})
To use this however, you need to make sure that getClient returns a promise. I suggested you use ic-ajax (included in Ember). It's an abstraction over jquery ajax that returns promises instead of expecting success and error callbacks
Furthermore, I strongly suggest that you look into ember-data and try to build a backend compliant with the spec. This way, you have a nice abstraction for your data and it increase development velocity tremendously, since you don't have to worry about interaction with the backend.
Edit: Returning a promise instead of data will not impact your template logic. Ember will resolve the promise and the Ember run loop will update the template automatically when the value changes. What you might want to have though, is perhaps a default value or a spinner of some kind. However, this is probably something you would have done regardless of if your model was returning a promise or not.
I created an Angular factory for fetching JSON data. I'm using $resource with the get method to retrieve this JSON object from the server. The object itself contains child objects.
I've been trying to use the data I retrieved with this factory in my controller but when I call the $scope variable, I get some variation of this
Cannot read property propertyName of undefined.
I could read the property when I logged it to the console so I don't get why it just disappears. To diagnose it, I tried passing by reference another variable.
The problem is that I can find the object and its keys by logging the console. The problem is that when I try to use this data, the object keys become undefined. I have no idea why this happens.
NewOrder.get({"id": $stateParams.loopId, "orderId": $stateParams.orderId}).$promise.then(function(order) {
$scope.myData = order["data"];
console.log("this is an order", order);
});
Here is my factory
angular.module('myApp')
.factory('NewOrder', function($resource) {
return $resource('/api/loops/:id/orders/:orderId');
});
I found out that if I create a new variable and set it equal to the value of myData, the object inside my key disappears.
This works
$scope.getOrder = function() {
console.log($scope.myData);
}
=> Object {recruitingLeague: "NAHL", playerPosition: "LeftDefense", playerDOB: Object, playerHeight: Object, playerWeight: Object…}
Creating a new variable and passing by reference the value of the previous variable (for diagnosis purposes) doesn't.
$scope.newData = $scope.myData;
$scope.getOrder = function() {
console.log($scope.newData);
}
=> undefined
I cannot understand why the objects I'm retrieving from my server suddenly disappear.
The service is asynchronous, so $scope.myData isn't there when
$scope.newData = $scope.myData;
occurs, but it is already there when
$scope.getOrder = function() {
console.log($scope.myData);
}
is called.
I have a route that creates a new record like so:
App.ServicesNewRoute = Ember.Route.extend({
model : function() {
return this.store.createRecord('service');
},
setupController: function(controller, model) {
controller.set('model', model);
},
});
Then I bind that model's properties to the route's template using {{input type="text" value=model.serviceId ... }} which works great, the model gets populated as I fill up the form.
Then I save record:
App.ServicesNewController = Ember.ObjectController.extend({
actions : {
saveService : function() {
this.get('model').save(); // => POST to '/services'
}
}
});
Which works too.
Then I click the save button again, now the save method does a PUT as expected since the model has an id set (id: 102):
But then when I look at the PUT request in Dev Tools, I see that the id attribute was not serialized:
As a result, a new instance is created in the backend instead of updating the existing one.
Please ignore the serviceId property, it is just a regular string property unrelated to the record id which should be named just id.
I don't know why the id is not being serialized... I cannot define an id property on the model of course since Ember Data will not allow it, it is implicit. So I don't know what I am missing...
Any help is greatly appreciated!
The base JSONSerializer in Ember-Data only includes id in the payload when creating records. See DS.JSONAdapter.serialize docs.
The URL the RestAdapter generates for PUTting the update includes the ID in the path. In your case I believe it would be: PUT '/services/102'.
You can either extract it from the path in your backend service. Or you should be able to override the behavior of your serializer to add the id like this:
App.ServiceSerializer = DS.JSONSerializer.extend({
serialize: function(record, options) {
var json = this._super.apply(this, arguments); // Get default serialization
json.id = record.id; // tack on the id
return json;
}
});
There's plenty of additional info on serialization customization in the docs.
Hope that helps!
Initially I used ronco's answer and it worked well.
But when I looked at ember data's source code I noticed that this option is supported natively. You just need to pass the includeId option to the serializer.
Example code:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serialize: function(record, options) {
options = options ? options : {}; // handle the case where options is undefined
options.includeId = true;
return this._super.apply(this, [record, options]); // Call the parent serializer
}
});
This will also handle custom primary key definitions nicely.
Well, as far as I know it's a sync issue. After first request you do the post request and then, it has been saved in the server, when you click next time the store haven't got enough time to refresh itself. I've got similar issue when I've created something and immediately after that (without any transition or actions) I've tried to delete it - the error appears, in your case there's a little bit another story but with the same source. I think the solution is to refresh state after promise resolving.
In my Controller, I'm quering data from a $resource object bundled in a caching service.
$scope.data = myService.query(); //myService caches the response.
In the same controller I have the configuration for a chart (for the GoogleChartingDirectve).
$scope.chart = {
'type': 'AreaChart',
'cssStyle': 'height:400px; width:600px;',
....
'rows' : convert($scope.data),
...
}
convert is just a function which takes the data from myService and returns an array for the chart.
This is only working, if the response of the query has already been cached (After changing the route, I can see the graph, but if I call the route directly it is empty).
Perhaps the problem is my caching?
angular.module('webApp')
.service('myService', function myService($cacheFactory,res) {
var cache = $cacheFactory('resCache');
return {
query: function() {
var data = cache.get('query');
if (!data) {
data = res.query();
cache.put('query', data);
}
return data;
}
};
});
I've tried the $scope.$watch in my Controller. It is fired only once with no data. If I change to a different route and back again it is fired again; this time with data.
$scope.$watch('data',function(newValue){
console.log("Watch called " + newValue);
}
I'm not sure what the problem actually is.
It looks like the $watch event is missing.
If I call the refresh with a button, $watch is fired and data is shown.
$scope.refresh = function(){
$scope.data = $scope.data.concat([]);
}
I'm just an angular newbie and playing around in a small demo project.
So I'm looking for the best way to solve some common problems.
Any idea how to solve my problem?
I guess the problem is that you are not aware that res.query() is an asynchronous call. The $resource service works as follow: the query call returns immediatly an empty array for your data. If the data are return from the server the array is populated with your data. Your $watch is called if the empty array is assigned to your $scope.data variable. You can solve your problem if you are using the $watchCollection function (http://docs.angularjs.org/api/ng.$rootScope.Scope):
$scope.$watchCollection('data', function(newNames, oldNames) {
var convertedData = convert($scope.data);
...
});
Your cache is working right. If you make your server call later again you got the array with all populated data. Btw. the $ressource service has a cache option (http://docs.angularjs.org/api/ngResource.$resource) - so you don't need to implement your own cache.
You may ask why the $ressource service works in that way - e.g. returning an empty array and populating the data later? It's the angular way. If you would use your data in a ng-repeat the data are presented to the view automatically because ng-repeat watches your collection. I guess you chart is a typical javascript (for example jQuery) that doesn't watch your data.
Trying to populate a Collection from a list of values, I am getting an error about the Collection's model's prototype being undefined. Looking at this question about a similar problem, I have checked that the Model is actually created before the collection is instanced, to the best of my ability.
The error is being thrown in one of the event handlers of the Marionette CompositeView that holds the Collection, after fetching the data from the server and trying to reset the collection with the list of values from the data which should be populated into it.
Note: Using Backbone 0.9.10
The Model
MyItemModel = Backbone.Model.extend({});
The Collection
MyCollection = Backbone.Collection.extend({
model: MyItemModel
});
The CompositeView's relevant code
MyCompositeView = Backbone.Marionette.CompositeView.extend({
initialize: function(options) {
_.bindAll(this);
this.model = new MyCompositeViewModel();
this.collection = new MyCollection();
},
//This event handler gets properly fired and run.
on_event: function() {
var that = this;
// The data comes through fine with this `fetch`
this.model.fetch({success: function() {
var collection_results= that.model.get("collection_results");
// The error fires in this line
that.collection.reset(collection_results);
that.render();
});
}
})
The error
The error happens in the add function in Backbone, when doing a get for the model object, checking to see if it is a duplicate. The failing code is here:
// Get a model from the set by id.
get: function(obj) {
if (obj == null) return void 0;
// The error originates from this line
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
},
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
Here, the this.model.prototype.idAttribute fails because the prototype for the model is not defined.
Why is this happening, and how can it be fixed?
Thanks a lot!
The reason is, in Babkbone 0.9.10, if you call collection.reset(models) without options, the models will be passed to collection.add() which strictly needs real models as argument.
But, in fact, the arguments you passed are not real models. They are just an array of hash attributes.
Two options to fix:
Option 1: Call the reset with a parse option
that.collection.reset(collection_results, {parse: true});
Then reset will parse the array of hashes and set them as model.
Option 2: Upgrade to latest version Backbone 1.1.0.
Here reset() no longer pass responsibility to add() but use set() smartly. This option is recommended. And you don't need options here.
that.collection.reset(collection_results)
Another point
May I suggest you not to define model in CompositeView? CompositeView is for collection, not model. Of course I understand the model here is just to hold and fetch some data, but it would be really confusing for the code to be read by another developer, as well as your own maintaining.
To get bootstrapped data, you can load the data at first request and use conventional way to put it into collection. http://backbonejs.org/#FAQ-bootstrap