I'm new with Backbone and I think I misunderstand the use of get/set with the model. I made a really simple example with IPython widget :
class Automaton(widgets.DOMWidget):
from IPython.display import Javascript
_view_name = traitlets.Unicode('AutomatonView', sync=True)
nodes = traitlets.List(sync=True)
%%javascript
require(['widgets/js/widget'], function(WidgetManager){
var AutomatonView = IPython.DOMWidgetView.extend({
render: function(){
var n_nodes = this.model.get("nodes").slice();
n_nodes[0] += 1;
this.model.set("nodes", n_nodes);
return this;
},
});
WidgetManager.register_widget_view("AutomatonView", AutomatonView);
});
So now I can call the widget like that:
a = Automaton(nodes=[1])
What I understand here is: nodes is passed to the model and now in the model nodes = [1], then I create a new node [2] that I'm setting to the model, so I'm expected for the nodes value be [2] now, but If I ask
a.nodes
It's say:
[1]
Sorry if I don't understand something really basic, if you can explain whats is happening here it will be really useful for me. I checked if the n nodes is [2] with a console log and it's fine so it's really with "this.model.set" the problem.
Are you sure you're rendering the view? Try to put this code into an initialize method instead, and everything should work just fine. By the way, this should work even without a set method call:
initialize: function() {
var n_nodes = this.model.get("nodes");
n_nodes[0] += 1;
return this;
},
Maybe that can help someone but I fixed my error with adding
this.touch();
I found my answer on this example : http://nbviewer.ipython.org/github/ipython/ipython/blob/2.x/examples/Interactive%20Widgets/Custom%20Widgets.ipynb
" it is very important that we call this.touch() to let the widget machinery know which view changed the model "
Related
So I was what the best way for all views in an application to have actions performed on an element.
In a non single page application you would run say:
$(document).ready(function() {
$('.autosize').autosize();
});
to apply autosize function to all elements with the autosize class on every page.
Now in a Backbone Marionette app to do this you could perform that in each view with onDomRefresh or similar but for things that affect 90% of views you'd want this to run automatically somehow.
I don't think there's a way that an Application object can listen to all onDomRefresh events which would potentially solve it. I've consider overloading Marionette.MonitorDOMRefreshto add this in but it doesn't feel like a Backbone approach.
Other things I considered were sub-classing each of the marionette views to add mixins for loading different groups of UI elements.
I figured other people must have experienced this scenario so was interested what approaches have been used.
Just make a base View class and inherit from it every view class that needs the autosize enhancement.
var AutosizeBaseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function(){
this.$('.autosize').autosize();
}
});
then make your classes like this:
var SomeView = AutosizeBaseView.extend({
});
So I couldn't really find any solutions that really solved my problem, despite some helpful chats with #julio_menedez and #marionettejs on Twitter. With a really good idea being using Polymer but wasn't suitable as I need to support older IE's.
So instead I headed into the dangerous world of monkey patching to solve it (Bear in mind I might need to iron out some wrinkles with this still, just finished writing it and not fully tested it - I'll update accordingly)
In Coffeescript: (javascript version at the bottom)
# Monkey patching the Marionette View.. sorry!
# this is the only Marionette view which doesn't have it's own constructor
Marionette.ItemView = Marionette.ItemView.extend
constructor: ->
Marionette.View.prototype.constructor.apply #, Array.prototype.slice.call(arguments, 0)
original_view_constructor = Marionette.View.prototype.constructor
Marionette.View.EventAggregator = event_aggregator = _.extend {}, Backbone.Events
# all the other constructors call this so we can hijack it
Marionette.View.prototype.constructor = ->
event_aggregator.listenTo #, 'all', =>
args_array = Array.prototype.slice.call arguments, 0
event_aggregator.trigger.apply event_aggregator, [ 'view:' + args_array[0], # ].concat(args_array.slice(1))
event_aggregator.stopListening # if args_array[0] == 'close'
original_view_constructor.apply #, Array.prototype.slice.call(arguments, 0)
And then to use I just setup a listener in my application object to catch view events I need. e.g:
#listenTo Marionette.View.EventAggregator, 'view:dom:refresh', (view) ->
view.$('div').css('backgroundColor', 'red');
So in my view these are the pros and cons of this technique:
Pros:
Can listen to all view events without injecting all view classes or subclassing all view classes
Simple to use
Objects don't need to opt-in to using it at all
Cons
Uses monkey patching, dangerous to Marionette API Changes
Uses Marionette namespacing so vulnerable to a future Marionette namespace collision
Takes dealing with views out of view context
Having an event aggregator object isn't something seen elsewhere in Backbone/Marionette (afaiw) so breaks a pattern (update - something similar is seen with Backbone.history)
Anyway I'm welcome to feedback, alternatives, criticism :-) and hope maybe this helps someone else in the same situation
Javascript:
(function() {
var event_aggregator, original_view_constructor;
Marionette.ItemView = Marionette.ItemView.extend({
constructor: function() {
return Marionette.View.prototype.constructor.apply(this, Array.prototype.slice.call(arguments, 0));
}
});
original_view_constructor = Marionette.View.prototype.constructor;
Marionette.View.EventAggregator = event_aggregator = _.extend({}, Backbone.Events);
Marionette.View.prototype.constructor = function() {
var _this = this;
event_aggregator.listenTo(this, 'all', function() {
var args_array;
args_array = Array.prototype.slice.call(arguments, 0);
event_aggregator.trigger.apply(event_aggregator, ['view:' + args_array[0], _this].concat(args_array.slice(1)));
if (args_array[0] === 'close') {
return event_aggregator.stopListening(_this);
}
});
return original_view_constructor.apply(this, Array.prototype.slice.call(arguments, 0));
};
}).call(this);
In CoffeeScript I think you could also do:
extend = (obj, mixin) ->
obj[name] = method for name, method of mixin
obj
include = (klass, mixin) ->
extend klass.prototype, mixin
include Marionette.View,
onDomRefresh: () -> #$('.autosize').autosize()
Which should cover all the view types. Haven't tested this specifically, but just did something very similar to add functionality to Marionette's Layout view. Extend / include pattern at http://arcturo.github.io/library/coffeescript/03_classes.html. Of course this should all be doable in straight up JS too.
UPDATE:
Actually, since we have Underscore available to us we don't need to manually define the include and extend methods. We can just say:
_.extend Marionette.View.prototype,
onDomRefresh: () -> #$('.autosize').autosize()
I know this error has come up a few times, but I'm still not sure how to make this work appropriately..
My magic begins here :
var list_edit_member_view = new app.views.ListMemberEdit({
el: $("#enterprise_member_list_edit_container"),
list_ids: list_ids
});
list_edit_member_view.render();
And this loads this View (ListMemberEdit.js) which has this in the render() :
this.list_edit_member_view = new app.views.CollectionView({
el: $("#enterprise_member_list_edit_container"),
collection: app.peers,
list_item: app.views.ListMemberEditSelection,
list_item_options: {list_ids: this.options.list_ids}
});
Which loads a CollectionView view that renders its list_item_options as model views.. It is within this file (ListMemberEditSelection.js), that when I perform this.destroy, it will return :
Uncaught Error: A "url" property or function must be specified
So this makes me think that the Model or the Model URL is not being defined.. I'm just not sure where to put this since it works very similar to my other partials that are doing roughly the same thing..
Any thoughts? My apologies for the vagueness. Let me know if there's anything else you would like to look at!
I'm curious if its possible to see where this URL attribute would be written within the Object Model or Collection itself.
This is because destroy() function will call Backbone.sync to update the server too, not only your models in the frontend. http://backbonejs.org/#Model-destroy
So, if you're using REST to sync your data, you'll need to set a url property in your model so Backbone know where to send request:
Backbone.Model.extend({
url: "http://myapi.com/"
})
To allow more flexibility, you can also set a urlRoot: http://backbonejs.org/#Model-urlRoot
I had a similar problem, I removed the "id":"" from my models default values and the problem was solved.
I did receive similar error
Try this: I am just making an assumption what your model might look like
window.MyModel = Backbone.Model.extend({
url: function(){
return this.instanceUrl;
},
initialize: function(props){
this.instanceUrl = props.url;
}
}
Please look at this question that I had posted myself for more details: https://stackoverflow.com/a/11700275/405117
I am providing this reference as the answers here helped me better understand
Hope this helps!
I wanted to update the rank attribute of an existing model which I passed from another view. However, I get the error Uncaught TypeError: Object # has no method 'set'.
In the initialize part of the view, I have :
this.collection = new tgcollection({model : this.options.model });
I define a function updateModel intended to update the attribute value as:
updateModel: function(){
var val= $("#textbox_id").val();
console.log(val);
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
this.options.model.set({"rank": val});
this.render();
//
},
Where am I going wrong?
I can see the value and the model with its previous attribute values in the console.
The model:
define(['jquery','underscore', 'backbone', 'deepmodel'],
function($,_, Backbone) {
var model = Backbone.DeepModel.extend({
// Default attributes for the model.
defaults : {
id: null,
rank: null,
},
initialize: function(){
_.bindAll(this,"update");
this.bind('change : cost', this.update);
},
update: function(){
console.log(this.get("cost"));
},
// Remove this model from *localStorage*.
clear : function() {
this.destroy();
},
});
return model;
});
Just do
this.model.set({"rank": val});
instead of
this.options.model.set({"rank": val});
The model within a view is accessed via this.model not this.options.model
I love a good mystery. Here is my best guess based on what I see. The problem is probably even further back. Where you call:
this.collection = new tgcollection({model : this.options.model });
this.options.model is probably not what you think it is. It would be helpful to see the view BEFORE this view that is instantiating and passing in this.options.model. BTW, with models and collections passed into the view, you can always shorten it to this.model Model, Collection and a handful of others are special in that they get attached directly to the View once passed in.
I'm assuming that in your updateModel() the following SEEM to work:
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
The error is coming up on the set(), not the lines above. So the assumption is that you passed in a model. Or did you? My wild guess is that what this.options.model actually is, is just a json object of your model. This might explain why you "see" the model in your console when you stringify it, but then Backbone protests when you call set() on it.
Instead of JSON.stringify to test this.options.model try just console.log(this.options.model). Well, you don't have to test really. The fact that Backbone can't find set() on this object is a tell tale sign. If you're not seeing the complexity of a Backbone model in your console - it's not a model.
Also, for testing and debugging particularly models, I tend to use the model.toJSON() function as a quick check that it's a model and I'm seeing attributes I expect.
Let us know if you have more clues.
Tell me please, what is best practice of data processing inside of view?
Example: I have a User model and there is field age. In this field is an age of user. It is an integer value - amount of months. And how can I implement this in my template:
17 => 1 year 5 months
11 => 11 months
24 => 2 years
Where can I store this helper-method? Inside of template is incorrect way. Otherwise I need to do some function that will generate correct model json. (not model.toJSON()) or extend existed JSON... Or ....
What is the best way to do this?
Thanks.
i gave an answer to a similar question months ago:
find it in this question backbone toJSON with helper methods
it comes down to adding methods to the json, before you go to the template
like this:
var userModel = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'fullname', 'toFullJSON');
},
fullname: function(){
return this.get('name') + " " + this.get('lastname');
},
toFullJSON: function(){
var json = this.toJSON();
return _.extend(json, {fullname : this.fullname()});
}
});
var user = new userModel();
user.set({name: 'John', lastname: 'Doe'});
// you will see in this console log, that the toFullJSON function returns both the toJSON properties, and your added propert(y)(ies)...
console.log(user.toFullJSON());
another thing you could do is override the toJSON method
like this:
var myModel = Backbone.Model.extend({
// other methods and functions go here...
toJSON: function (attr) {
var defaultJSON = Backbone.Model.prototype.toJSON.call(this, attr)
return _.extend(defaultJSON, {calculateAge : this.calculateAge()});
},
calculateAge: function(){
// here you calculate the years and what not, then return it.
}
});
and a third way of doing this would be to give the model to your template instead of the .toJSON() return. then you can call model.CalculateAge() in your template.
There are two approaches.
You can put it on the model and then pass model to the template - it will mean that in the template you'll have to get the attributes using model.get('age') etc. but it will also make it possible to use helper methods for this model.
Another option is to have some kind of global helpers collection that later on you could access from your template like helpers.verboseAge(age) (don't know what templating scripts you are using so it could be it.age, this.age, age... but you get the idea.
The standard way to use the localStorage plugin for Backbone.js works like this:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
},
localStorage : new Store('English')
}
But I want to make different, parallel wordlist collections in different languages. So, I want to be able to instantiate the name of the Store upon initialization of the collection. AFAICT, this works ok:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
this.localStorage = new Store(options.language);
}
}
Then I can instantiate a WordList like:
english = new Wordlist([], {language: 'English'});
Or:
chinese = new Wordlist([], {language: 'Chinese'});
The thing is, I haven't really seen this done in any other examples and I'm wondering if anyone out there would have any "Eek! Don't do that, because..." sorts of reactions.
EDIT
I should add that I have already tried doing it this way:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
},
localStorage : new Store(options.store)
}
And then:
chinese = new Wordlist([], {language: 'Chinese'});
But for some reason options.store is coming up undefined.
It's easier to explain myself as an answer, so I'll go ahead and give one.
In:
App.WordList = Backbone.Collection.extend({
initialize : function(models, options){
....
},
localStorage : new Store(options.store)
})
This is really little different from
var newInstanceConfig = {
initialize : function(models, options){
....
},
localStorage : new Store(options.store)
}
App.WordList = Backbone.Collection.extend(newInstanceConfig);
Think of it this way; there's nothing magical about the object being passed in to Backbone.Collection.extend(...). You're just passing in an ordinary object. The magic happens when Backbone.Collection.extend is invoked with that object as a parameter
Thus, the options parameter of the object method initialize is completely different that which is being passed in to new Store(...). The function being assigned initialize is defining the scope of options. Who knows where the one referred to in new Store(options.store) is defined. It could be window.options or it could be options defined in some other scope. If it's undefined, you're likely getting an error
That being said, I only see two or three strategic options (oh jeez, forgive the pun please!).
Whenever you're creating a new instance of the collection, either:
Pass in the language and let your Backbone collection create the new Store(..) where needed.
Pre-Create the Stores and either pass or give the specific Store want to that instance (either directly through its constructor or maybe you have your constructor "look-up" the appropriate pre-created Store).
And finally, I guess you could delegate the task of creating stores to another object and have it implement either options one or two. (Basically a Store Factory/Resource Manager kinda thing).
What you need to figure out is which one of those strategies should work for you. I have never used localStorage so, unfortunately, I can't help you in that regard. What I can do is ask, is there ever going to be multiple instances created from App.Wordlist where there might accidentally be created two of the same kind of Store?
In fact, I've got another question. where is this Store defined? Are you sure that's not defined somewhere in one of your other API libraries you're using? Perusing the localStorage docs I know about mentions something of a Storage constructor but nothing of a Store. So you might want to figure out that as well.
Edit #1: Nevermind, I see you mentioned where Store was defined.
I got around this by creating a method which allows you to configure the localStorage after instantiation:
var PageAssetCollection = Backbone.Collection.extend ({
initialize: <stuff>
model: <something>
...
setLocalStorage: function ( storageKey ) {
this.localStorage = new Backbone.LocalStorage(storageKey),
},
});
you can then set the localStorage after you have set up the collection:
fooPageAssets = new PageAssetCollection();
fooPageAssets.setLocalStorage('bar');