I have a Backbone model containing submodels which are initialized in the "parse" method of the model such as:
parse: (response,options) ->
attrs = super
if attrs.child
attrs.child = new Child( attrs.child )
Whenever I call "set" on the parent model with a different instance of a model of the same type, a "change" event is always fired even if nothing in the parent or the child model has changed. See fiddle demonstrating this here: http://jsfiddle.net/inpursuit/ZP7k9/20/
It looks like the code for the set() method in backbone is using the underscore isEqual method to detect whether an attribute has changed:
if (!_.isEqual(current[attr], val)) changes.push(attr);
In the case where the attribute is a Backbone.Model the isEqual method will always return false even if the attributes of the child model are the same. Shouldn't this call to isEqual pass the attributes of the model if the item is a model? i.e something like this:
currrentAttr = if( current[attr] is Backbone.Model ) then current[attr].attributes else current[attr]
newAttr = if( val is Backbone.Model ) then val.attributes else val
if (!_.isEqual(current[attr], val)) changes.push(attr);
Bottom line, i'm trying to prevent a change event from being fired when a Model is set on another Model and all attributes of the two models are equal.
Thanks for the help!
Related
In cakephp 2.x I could define a variable in app model public $someVar = false; and it would be accessible in all models. I could even access/set it from controller for any model: e.g.
$this->User->someVar = true;
Since there is no app model, is there any way to achieve the same thing in cake 3. I have global event listeners set up as in this answer
Cakephp 3 callbacks, behaviors for all models
So, the purpose is to have a variable, which will be accessible in those global listeners, all models' callbacks as well as from controller through model's object - similar to cake 2: for app model's callbacks, all models' callbacks and from controller respectively.
edit: would not prefer to use Configure
Thanks
It looks like I figured it out
Create a Behavior and load it in initialize global event. similar to example here
Cakephp 3 callbacks, behaviors for all models
This way it will be available in all models and callbacks. Create a variable in that behavior public $someVar = null. However accessing directly the behavior's variable is not possible (as it is treated as an association)
https://github.com/cakephp/cakephp/issues/9153
So, you can define method to set/get the value
// inside Behavior
public function setSomeVar($val = null) {
if ($val === null) {
return $val;
}
return $this->myVar = $val;
}
To access/modify that variable
// inside callbacks/event listeners
$event->subject()->setSomeVar(); // to get
$event->subject()->setSomeVar('smth'); // to set
// from controller
$this->Users->setSomeVar(); // to get
$this->Users->setSomeVar('smth'); // to set
I have 2 post collections and a model as follows.
# router file
#posts = new MyApp.Collections.PostsCollection()
#posts.reset options.posts
#followed_posts = new MyApp.Collections.PostsCollection()
#followed_posts.reset options.followed_posts
# Post model file
class MyApp.Models.Post extends Backbone.Model
paramRoot: 'post'
follow_post: ->
# ajax call
console.log "_________Index:#{this.collection.indexOf(this);}"
console.log this.collection
console.log "_________Followed:"
console.log #followed_posts
class MyApp.Collections.PostsCollection extends Backbone.Collection
model: MyApp.Models.Post
url: '/posts_all'
What I am trying to do is when one of the model changed in one collection, I want to update the other model in other collection too.
These collections may or may not hold same models.
So let's say if a model in #posts changed in my Post model, I want to update that model in #followed_posts too. If #followed_posts doesn't have that model, I need to add a duplicate of the model to #followed_posts collection.
I can access the collection that model belongs, but I cannot access the other collection.
Any ideas appreciated, thanks.
If the two collections are antisocial and can't talk directly to each other, which is usually good design, then you'll need an intermediary -- a global event dispatcher. When a model changes, propagate that event to the dispatcher along with a reference to the model. Listen for the event in the other collection and use the model passed to check for existence and respond as needed.
EDIT:
Backbone's documentation mentions this pattern:
For example, to make a handy event dispatcher that can coordinate
events among different areas of your application: var dispatcher =
_.clone(Backbone.Events)
But in fact, this is such a common pattern that the Backbone object itself is extended with Events. So you can just do:
// In your Post model
#on "change", -> Backbone.trigger "post:change", this, #collection
// And then something like this in the collection class definition:
#listenTo Backbone, "post:change", (model, collection) =>
if post = #get model.cid
post.set model.toJSON()
else
#add model
Also, is followed posts a subset of posts? If so, why not put an attribute on the model designating it as followed? Then you could find all followed posts with a simple filter function.
I would strongly suggest that you should consider having a single collection and add some kind of attribute in the model to differentiate between what kind of posts they are.
I took a small code from backbone home site, and consoled the function, in this example, sidebar.on('change:color'), takes the function. but it requires two parameter, one is abiously we need that is 'color', and we defining the element inside the function, still why we giving another parameter as 'model' here, what that parameter does?
if i remove that parameter send only the color, the function doesn't work at all... any one help me to understand this?
sample function here:
var Sidebar = Backbone.Model.extend({
promptColor : function(){
var cssColor = prompt('Please enter a css color');
this.set({color:cssColor});
}
});
window.sidebar = new Sidebar;
sidebar.on('change:color',function(model,color){ // what model parameter do here?
console.log(model);
$('#sidebar').css({
background:color
})
})
sidebar.set({color:'green'});
sidebar.promptColor();
when i console the model i got this:
d
_callbacks: Object
_changing: false
_escapedAttributes: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object
changed: Object
cid: "c1"
__proto__: x
It is possible that you want to know which model was affected.
Consider a case where you are listening to an event on a collection instead. Which model's color value was modified? The model parameter tells you this.
Also, consider a case where the same handler is listening to "change:color" on multiple models. Again, you might want to know which model sent the event.
Just like in other event-driven environments, the "sender" is always passed along with the event data. model, in this case, is the sender.
Backbone is a Javascript MVC framework. (Unlike standard MVC, Backbone doesn't have controllers, instead it has collections).
The model you are receiving is a standard model from the MVC paradigm. Model's are the underlying data structures that hold the data that the user is working with.
When you do
sidebar.on('change:color', function(model, color) {
// some code here
});
you are attaching an event handler to the sidebar model. Specifically, you are saying that when the color attribute on this model changes, call the function. Since this event can and will trigger at a later point in time, Backbone passes the event handler function two arguments: the first is the model on which the event fired, and the second is the attribute that changed.
The arguments are passed in a specific order, that is model is the first argument, and the changed attribute is the second. Thus if you omit the model argument from your event handler function, the passed in model gets assigned to color, and color doesn't get assigned to any argument.
Recommended reading:
More about MVC and models
More about backbone models
The Backbone documentation says:
Model.set will fail if validation fails - it won't set the value therefore it won't trigger any callback. We can pass { silent: true } to Model.set - then it will set the value but won't trigger any callback neither.
So,
Why does Backbone Model require a valid state to simply set an attribute value? What if we want to set attributes as the user interacts with the UI, but the model is not valid yet? It means change callbacks are unavailable unless we pass { silent: true } then manually trigger the change?!
Please say you know a better way of handling this :)
I'm not sure how to answer the Why questions but you could say that there are arguments for why it is good that set runs validations. For instance, it makes it dead simple to do client side validation in real time.
If your problem could be solved by only validating the value that is currently being changed by the user, you can do that by combining your validate method with the hasChanged method.
For example something like this:
Backbone.Model.extend({
defaults : { name : "" },
validate : function (attrs) {
var errors = {};
if(this.hasChanged("name") && attr.name.length == 0) {
errors.name = "Need a name yo!";
}
//...
if(_.keys(errors).length > 0) {
return errors;
}
}
})
In Backbone whenever you call set on model , It keeps track of what attributes of model has been changed and what attributes are newly added.Calling validate allows it be more efficient in doing it .Passing {silent:true} as options in set function causes validate and change to not execute so if doesnt fire any change events.
If you want to set attributes as the user interacts with the UI, but the model is not valid yet
In this case you can set the change in a plain object make sure object keys are sames as model's attribute and then at some point just set in your model.
var uiChanges = {name:'x'}; //just fill it with your changes
ur_model.set(uiModel); //then set it ,this way it fires change events only once
To check the diff between your plain object and model you can use the
ur_model.changedAttributes(uiChanges);
changedAttributes -
Return an object containing all the attributes that have changed, or false if there are no changed attributes.
You can further use it save only those attributes that have changed rather than saving entire model again.
A Backbone.Model object maintains a 'collection' attribute.
In the Backbone source code, we have :
var Model = Backbone.Model = function(attributes, options) {
...
if (options && options.collection) this.collection = options.collection;
...
}
The annotated source says nothing about that, and the documentation references only functions.
I guess it's mainly used when deleting the model : the collection removes this model.
But what if the model belongs to many collections ? Is there a danger to use/override this collection attribute ?
It's used in two places (look a bit further down in the annotated source).
As you note, it's used when a model is destroyed, to remove the model from the collection.
Optimistically removes the model from its collection, if it has one.
In the url function, if the collection exists with a URL scheme, it uses that to construct a URL for the model.
if you're using Backbone's restful methods, override this to change the endpoint that will be called.