I've got this Backbone.Model representing a Google Books API volume:
var Book = Backbone.Model.extend({
defaults: {
volumeInfo : {
title: 'n.a.',
authors: 'n.a.',
publisher: 'n.a.',
publishedDate: 'n.a.',
imageLinks : {
smallThumbnail: '/unavailable.jpg'
}
}
},
parse: function(resp) {
if (resp.volumeInfo.authors) {
resp.volumeInfo.authors = resp.volumeInfo.authors.join(',');
}
return resp;
}
});
Which is fed to this template:
<script type="text/template" id="bookCollectionRow">
<tr>
<td><img class="thumbnail" src="<%= volumeInfo.imageLinks.smallThumbnail %>" /></td>
<td><a target="_blank" href="<%= volumeInfo.canonicalVolumeLink %>"><%= volumeInfo.title %></a></td>
<td><%= volumeInfo.authors %></td>
<td><%= volumeInfo.publisher %></td>
<td><%= volumeInfo.publishedDate %></td>
</tr>
</script>
Upon parsing the template, when a volume JSON does not contain an imageLinks I receive this error:
Uncaught TypeError: Cannot read property 'smallThumbnail' of undefined.
I know I could fix it by checking with an if in the Model or in the template but what's the purpose of defaults model property then? Does that work only if not overriding parse?
A few things. First, you shouldn't have nested objects as backbone model attributes in general - it can be OK if you can always treat the attribute atomically, but this is a perfect example of when you can't. From a data-model perspective, imageLinks should be its own backbone model class, as should volumeInfo.
Second, if defaults is an object literal ({}) instead of a function, the same object is used as the default attrs for each model instance. I think you want this:
defaults: function(){
return {
volumeInfo : {} // should be new VolumeInfo({}) imo
};
},
But the data model is the bigger issue - .defaults doesn't do the kind of nested-object-template thing you seem to be going for, and for good reason: it doesn't work well, this will just be the first of many gotchas you'll run into if you don't keep your instance data pretty flat.
Related
I have a div, listing properties of the object POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}. The object POI si owned by a Service. The directive's controller has the function getPoi() that gets the POI from the service, and returns it to the directive.
My current HTML is something like this:
<table ng-controller="Controller as ctrl">
<tr> <!-- address -->
<td>{{ctrl.getPoi().address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{ctrl.getPoi().phone}}</td>
</tr>
<tr> <!-- website -->
<td>
<a ng-href="{{ctrl.getPoi().website}}">
{{ctrl.getPoi().website}}
</a>
</td>
</tr>
</table>
The controller
.controller('Controller', function(CurrentPoiService)
{
this.getPoi = function()
{ return CurrentPoiService.POI; }
}
The service
.service('CurrentPoiService', function()
{
this.POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}
}
In this way I am adding 3 watchers. Is there a way to add just 1 watcher, since it's the same parent object? Here it is a JSFiddle
Thank you
[UPDATE 1]
This is the (still not working) JSFiddle using the solution proposed by #Charlie
[UPDATE 2]
This is the working JSFiddle
As Claies has mentioned in a comment, you should never call your data from
the view through a function this way.
In this scenario you can create a watch for the POI object with the objectEquality argument true to watch the properties of the object in a single $watch. Then find your elements inside the listener and change the value in the way you want.
$scope.$watch('POI', function(){
//Assume that $scope.propertyIndex is the current property to display
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.address);
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.phone);
//etc...
}, true);
You have a better control this way. But please keep in mind that this method is not suitable if POI is a complex object.
UPDATE:
Here is a working example of showing a random number every second using a watch and a factory. You should be able to learn from this and apply it to your project.
myApp.controller('myController', function($scope, dataSource) {
$scope.poi = {rand: 0};
$scope.$watch('poi', function() {
$('#rand').html($scope.poi.rand);
}, true);
dataSource.open($scope);
});
myApp.factory('dataSource', function($interval) {
return {
open: function(scope){
$interval(function() {
scope.poi.rand = Math.random();
}, 1000);
}
}
});
Try inside your controller :
$scope.POI = ctrl.getPoi();
HTML :
<tr> <!-- address -->
<td>{{POI.address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{POI.phone}}</td>
</tr>
I have the following angular code:
<tr ng-repeat="vm in ...">
<span ng-if="myLookupFunc(vm)"> {{myLookupFunc(vm)).label}}, {{myLookupFunc(vm).uuid}}
<span ng-if="!myLookupFunc(vm)">-</span>
</tr>
As you can see myLookupFunc is called 4 times for a single item.
How can this be optimized so that it is called only once for a given 'vm' instance?
I did try to use ng-init at 'tr' level but it doesn't re-evalute after properties of 'vm' change -- and it is expected, according to the documentation ng-init should not be used for such cases.
So what is a proper way in angularjs to accomplish this?
Please check the below code for better optimization.
<tr ng-repeat="vm in ..." ng-init="lookupData=myLookupFunc(vm)">
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}
<span ng-if="!lookupData">-</span>
</tr>
ng-init will run once on the start of the ng-repeat. That's why it doesn't change for you.
You have to use the controller to fetch the variable data, but you can improve it.
One way is to do this on your controller:
function lookup(vm, firstRun) {
if (firstRun) {
$scope.lookupVar = myLookupFunc(vm);
}
else {
return $scope.lookupVar;
}
}
and then you can keep your html code almost the same:
<tr ng-repeat="vm in ...">
<span ng-if="lookup(vm, true)"> {{lookup(vm)).label}}, {{lookup(vm).uuid}}
<span ng-if="!lookup(vm)">-</span>
</tr>
A better solution would be to just keep one span in the HTML and then do:
<tr ng-repeat="vm in ...">
<span>{{getVmText(vm)}}</span>
</tr>
And define a function getVmText on the controller that checks for VM value and returns the text. I believe this is the preferable way.
Use a data model and update it whenever necessary (even when "external events" occur). This way you don't have to bother with "re-evaluating controller functions", it's just pure angular data binding.
Example:
$scope.vms = ["id1", "id2", "id3"];
// this var will hold your data model (it could also be on the $scope, but for
// demo let's leave it like this)
var data = {
"id1": {
uuid: "123-123",
label: "label 1"
},
"id2": {
uuid: "456-456",
label: "label 2"
},
"id3": {
uuid: "abc-abc",
label: "label 3"
}
};
$scope.myLookupFunc = function(id) {
return data[id];
};
And then you can use it like this:
<div ng-repeat="vm in vms" ng-init="lookupData=myLookupFunc(vm)">
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}</span>
<span ng-if="!lookupData">-</span>
</div>
Plunker
OK, after considering the suggestions above, I found what I think is the easiest solution.
We just need to run
{{lookupData = myLookupFunc(vm)}}
and after that we can reference 'lookupData' variable. If we just run the code above inline, it will also evaluate and show the result (as JSON stringified text) inline, which is not what we want. So I ended up creating a dedicated directive which is a noop directive:
app.directive("ngAssign", function () {
return {
restrict: 'A'
};
});
Then one can just say:
<tr ng-repeat="vm in ..." ng-assign={{lookupData = myLookupFunc(vm)}}>
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}
<span ng-if="!lookupData">-</span>
</tr>
Full plunker: http://plnkr.co/edit/34DwCGR7Po8zg2mnCAtB?p=preview
I've come across this backbone / underscore template binding issue that I can't get my head around.
The underscore template is being provided a collection of objects however when traversing the collection and building the resultant HTML not all the individual object attributes are being resolved.
I believe the template is receiving the collection correctly (asynchronous call to the server). I've debugged the received collection and its populated correctly.
I've "rendered" the raw collection items within the HTML to verify I'm dealing with the correct objects...all appears correct.
I've simplified the code here in the hopes of not blurring the issue. A click event is responsible for selecting a section(not included in code)
Here is the code:
//Underscore Template
<script id="articles-template2" type="text/html">
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Date</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<% _.each(articles, function(a){ %>
<tr>
<td><%= JSON.stringify(a)%></td>
<td><%= a.title %></td>
<td><%= a.description%></td>
<td><%= new Date(a.date)%></td>
</tr>
<%});%>
</tbody>
</table>
</script>
// Backbone View ----------------
window.ArticlesView2 = Backbone.View.extend({
initialize: function () {
var self = this;
self.collection.on("reset", this.render, this);
},
template: _.template($('#articles-template2').html()),
render: function () {
var self = this;
$(self.el).html(self.template({ articles: self.collection.models }));
$('#section-list').append(self.el);
return self;
},
events: {
"click a": "clicked"
},
clicked: function (e) {
var self = this;
e.preventDefault();
var id = $(e.currentTarget).data("id");
var item = self.collection.get(id);
var name = item.get("title");
alert(name);
}
});
// Router ----------------
var AppRouter = Backbone.Router.extend(
{
routes: {
'section/:title': 'viewSection'
},
viewSection:function(section)
{
var self = this;
self.articles = new ArticleList({ selectedSection: section });
self.view = new ArticlesView2({ collection: self.articles });
self.articles.fetch({ reset: true });
}
}
);
// Initialize ----------------
var app = new AppRouter();
Backbone.history.start();
app.navigate('section/Home', { trigger: true });
The rendered HTML is as follows :
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Date</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr>
<td>{"id":"527c61082241f813c09c7041","title":"title here","description":" test descript here","date":"2005-02-08T05:00:00.0000000Z"}</td>
<td></td>
<td></td>
<td>Invalid Date</td>
</tr>
</tbody>
</table>
I'm not sure why one can Stringfy() the object and get data but fail to interrogate its attributes successfully?????
Is this a event issue, man what am I missing?
Thanks
You're passing the raw array of your models to your template and a model is not a hash, you don't have direct access to the properties.
Either use collection.toJSON
self.template({ articles: self.collection.toJSON() })
or use model.get in your template
<%= a.get('title') %>
Note that in your example JSON.stringify will give you the expected representation of your data because
toJSON behavior
If an object being stringified has a property named toJSON whose value
is a function, then the toJSON method customizes JSON stringification
behavior: instead of the object being serialized, the value returned
by the toJSON method when called will be serialized.
and your models have a toJSON method.
I'm using Backbone.JS with Mustache, so to render my tempaltes I call MyModel.toJSON(). This leaves me with only access to attributes. How can I have some attributes that are always calculated?
I looked at the Backbone.JS documentation and it might work to override validate() but this seems like a hack and may lead to infinite loops.
I also tried making an attribute be a function instead of a value, but Mustache doesn't get a value when I try to use it.
This is how I'm currently doing it. I do the calculations when initializing a model, and adding a listener for changes to the model to recalculate automatically.
...
initialize: function() {
console.log('Lead:initialize');
_.bindAll(this, 'validate', 'calculate');
this.bind('change', this.setCalculations, this);
this.setCalculations();
},
setCalculations: function() {
this.set({ calculations: this.calculate() }, { silent: true });
},
calculate: function() {
// do the calculations and return
},
...
I dont know if i understand the question correctly, but:
Can't You pass the actual model to mustache? so for example when you render
render: ->
rendered_content = #template({model: #model})
$(#.el).html rendered_content
#
You are passing the actual model to template. Then you have a template
<td class="quantity">
<input type="text" value="<%= model.get('quantity') %>" name="quantity" />
</td>
<td>
<%= model.getTotalPrice() %>
</td>
And in model you declare getTotalPrice()
getTotalPrice: ->
total_price = #get('price') * #get('quantity')
total_price + total_price * #get('tax_rate')
I actually never pass #model.toJSON in my templates, alawys the actual model.
I have the following backbone.js client side template:
<script id="calleeTemplate" type="text/x-jquery-tmpl">
<tr style="background-color: ${StatusColour}">
<td class="responder">${ContactFullName}</td>
<td class="status" style="width:200px">${Status}</td>
<td class="replied">${Replied}</td>
<td class="wauto">${Response}</td>
</tr>
</script>
In order to be able to bind to these properties, I have the following render method of a view:
App.Views.Callees = Backbone.View.extend({
initialize: function () {
},
el: $('#calleesStatuses'),
render: function () {
var col = _.map(this.collection.models, function (item) {
return item.attributes;
});
$('#calleesStatuses').html('');
$('#calleeTemplate').tmpl(col).appendTo(this.el);
}
});
I have to extract the attributes using the underscore _.map function from the model. I think the reason for this is because backbone uses the .get("property") to extract the property value.
This does not seem right to me, am I doing anything wrong?
You're right, you have to transform the data in order to be able to easily use it with tmpl.
However, it's better practice to use toJSON rather than accessing attributes directly. It's probably best to avoid calling the .models directly as well.
You don't need to anyway, backbone collections have a full suite of underscore.js enumerators. So you can cut your transformation down to one line:
var col = this.collection.invoke('toJSON')