Here is my model:
var SendAlertsModel = Backbone.Model.extend({
defaults: {
customSubject: "",
customNote: "",
userList:[],
alertUserList:[]
}
});
Inside view:
initialize: function(options) {
var self= this;
if(_.isUndefined(options)===false){
self.model= new SendAlertsModel();
self.loggedInUser = app.user;
self.model.set("userList",options.previousTabData.get("userList"));
self.model.set("alertUserList",options.previousTabData.get("userList"));
self.model.get("alertUserList").push(self.loggedInUser);
}
},
The issue which i am facing here is when i push the loggedInUser to alertUserList array, it automatically pushes the same loggedInUser to userList array.
Please give your suggestions on this.
Thanks
//create new alerts model
model = new SendAlertsModel();
//assign logged in user to local variable
loggedInUser = app.user;
//set the user list to be the user list from the previous tab data
model.set("userList",options.previousTabData.get("userList"));
//set the alertUserList to be the previous tab user list by reference
model.set("alertUserList",options.previousTabData.get("userList"));
//push the logged in user into the alert user list
model.get("alertUserList").push(self.loggedInUser);
I think the issue occurs when you set the alertUserList to be the userList. As the user list is an object the alertUserList now contains a reference to the userList. It's not a copy. When you update the alertUserList you are actually updating the userList too.
Think of it like this:
var alertUserList = userList = {some object in memory};
In this line here you will want to create a copy rather:
model.set("alertUserList",options.previousTabData.get("userList"));
I'm not sure of what data type userList is, so it will depend on that. If you only need a shallow copy then you could do this using your underscore/lodash library (I assume that is what the "_" is):
model.set("alertUserList",_.clone(options.previousTabData.get("userList")));
Related
I'm new to backbone and js. I have a general question regarding code structure. I'm currently building a webpage with a layout view and two regions. The left region is populated and shown on pageload. The right region is only shown and populated on a click in the left region. My controller looks like the following:
mainView.leftRegion.show(treeview);
...
treeItemView.on("childview:item:click", function(childview, item){
// Fetch collection of subitems from datasource
var fetchingSubitems = App.request("subitems:entity", item.get("id"));
...
// when done update item model with fetched collection
item.set({subitems : subitems});
var subitemsView = new app.subitemsList({
model : item
});
mainView.rightRegion.show(subitemsView);
}
My issue is that I want to show the right view outside of the click event and then update it, when the item model is being updated. But how should I structure it then?
you can create a general state Backbone model which contains subItems :
var State = Backbone.Model.extend({
defaults : {
subItems: []
}
});
send this model to the left and the right views on initialize :
state = new State();
var treeView = new TreeView({state:state});
var subitemsView = new app.subitemsList({state:state});
mainView.leftRegion.show(treeview);
mainView.rightRegion.show(subitemsView);
when you click on item you modify the state model :
treeItemView.on("childview:item:click", function(childview, item){
// Fetch collection of subitems from datasource
var fetchingSubitems = App.request("subitems:entity", item.get("id"));
...
// when done update state model
state.set("subItems",fetchingSubItems);
}
the right view must listen to change on subItems on state model :
this.listenTo(state, "change:subitems", this.changeCollectionAndRender);
the method "changeCollectionAndRender" must get the list of subItems from state model , update its collection and rerender :
changeCollectionAndRender:function(){
this.collection = this.state.get("subItems");
this.render();
}
I have a custom shopping cart object that I created and put it in the lib folder.
ShoppingCart = function ShoppingCart() {
this.Items = new Array();
this.grandTotal = 0.00;
}
ShoppingCart.prototype.addItem = function(Item){
this.Items.push(Item);
this.Items.sort();
this.calculateTotal();
}
I initialized the shopping cart and store it as Session.set('shoppingCart') during the page created phase.
Template.loginStatus.created = function() {
Session.set('loginShown',false);
if(!Session.get('shoppingCart')){ //set default if session shopping cart not exist
var cart = new ShoppingCart();
Session.setDefault('shoppingCart',cart);
}
Then when user click add item to cart, it will trigger this logic:
var cart = Session.get('shoppingCart');
cart.addItem(item);
Session.set('shoppingCart',cart);
Somehow, it does not work. When I take a look ad the chrome console it says undefined is not a function, pointing at cart.addItem(item) line. If I change it to this, it will work , but of course since everytime new shopping cart is created, I cannot accumulate items in the cart.
var cart = new ShoppingCart();
cart.addItem(item);
Session.set('shoppingCart',cart);
How should I store and retrieve the object from session properly? It looks like the returned object from the Session.get() somehow not considered as ShoppingCart. Did I miss any type cast?
As #Peppe L-G mentioned, you can only store EJSONs in Session. To store your custom object, you need to be able to manually transform it to and from EJSONs. Example:
_.extend(ShoppingCart, {
fromJSON: function(json) {
var obj = new ShoppingCart();
obj.grandTotal = json.grandTotal;
obj.Items = json.Items;
return obj;
},
});
_.extend(ShoppingCart.prototype, {
toJSON: function() {
return {
grandTotal: this.grandTotal,
Items: this.Items,
};
},
});
Then you can save it to Session:
Session.set('shoppingCart', cart.toJSON());
and restore:
ShoppingCart.fromJSON(Session.get('shoppingCart'));
I ran into the same problem. Essentially what is happening Meteor Sessions (and Collections) can only store EJSON types, so your ShoppingCart custom type is retrieved from the Session as a normal Object.
While you can manually transform to and from EJSONs, you may end up needing to do this repeatedly in a lot of different places. If your ShoppingCart is a member of another object, you'll have to also manually transform the member. It's better to use EJSON.addType to tell Meteor how to handle it automatically anywhere you store or retrieve an object of that type.
There's a great demo of this here: https://www.eventedmind.com/feed/meteor-create-a-custom-ejson-type. Full docs are also here: http://docs.meteor.com/#/full/ejson. But a short version is this:
Add a method to your custom type called typeName:
ShoppingCart.prototoype.typeName = function(){
return "ShoppingCart";
};
Add another method called toJSONValue:
ShoppingCart.prototype.toJSONValue = function(){
/* return a JSON compatible version of your object */
};
And finally, add the custom type to EJSON with:
EJSON.addType("ShoppingCart", function fromJSONValue(value){
/* return an object of your custom type from the JSON object 'value' */
};
NOTE: the "Type Name" in steps 1 and 3 must match exactly.
According to backbonejs.org
All views have the el property at all times. Also, to link a view to an already existing element id, you should pass it in when instantiating the view.
In this small example below, I pass in the already existing id in as a string. When I log it to the console later it is logged incorrectly as an empty div. Was I suppose to pass in the actual element and not a string that specified the element id?
Either, way I find it strange that logging the id, logs an actual empty div with that id.
/***************************************************************************************************
*/
var ModelMediaPane = Backbone.Model.extend({
defaults: {
visible: false,
}
});
var model_media_pane = new ModelMediaPane();
var ViewMediaPane = Backbone.View.extend({
});
var view_media_pane = new ViewMediaPane({
model: model_media_pane,
id: 'mi_holder'
});
console.log(model_media_pane.get('visible'));
console.log(view_media_pane.id);
You don't pass an existing id like that, you do it like this:
var view_media_pane = new ViewMediaPane({
model: model_media_pane,
el: '#mi_holder'
});
Otherwise you're just setting the id to the created element.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
What triggers the comparison:
I have a button inside a template with {{ action "isResponse"}}. This template's controller has an isResponse : function() {...}
The problem I have: The action is fired every time I click the button, but App.Answer.find() only returns content after the 2nd click. I'm wondering if this is because the Answer model hasn't loaded, but am unsure how to properly set up an observer for isLoaded in my example (if that is even the issue)
So how come App.Answer.find() returns empty the first time it's called??
App.ChoiceController = Ember.ObjectController.extend({
chosen: false,
isResponse: function() {
// successfully returns what I want from this controller's model
var questionId = this.get('question.id')
// gets DS.RecordArray of the model i'd like to compare with
var answers = App.Answer.find()
// filter to get a result that matches this.get('question.id')
var answer = answers.filter(function(ans) {
// returns all entries that match
if(ans.get('question.id') == questionId) { return true }
}, 'answers.isLoaded'); // this observer doesn't seem to hurt or help
// get the final value I need
var choice = answer.mapProperty('choice.id')
// if choice array is not empty, (should only have 1 element anyways)
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
})
Here are the models involved. Both include DS.belongsTo attributes
App.Choice = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
})
App.Answer = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
"choice" : DS.belongsTo('App.Choice')
})
App.Question = DS.Model.extend({
})
EDIT
Here is jsfiddle showing the behavior. Make sure to open your browser console to notice that each button requires 2 clicks for action isResponse to function properly. http://jsfiddle.net/iceking1624/QMBwe/
After reading your comment I've retought a solution to your problem and one possible way might be that you can define a AnswerController of type ArrayController (since it's for a collection of answers) and then setup this controller in your ApplicationRoute's setupController hook.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
Later on you can then require access to the AnswerController's data using the needs API with needs:['answers'] from inside whatever controller that needs access to the answers collection, and finally have access to the data with this.get('controllers.answer'). You can find here more info on the needs API.
See here a possible solution that works correctly, displaying the right choice already on the 1st click:
App.AnswerController = Ember.ArrayController.extend({});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model) {
this.controllerFor('answer').set('content', App.Answer.find());
}
});
App.ChoiceController = Ember.ObjectController.extend({
needs: ['answer'],
chosen: false,
isResponse: function() {
var questionId = this.get('question.id');
var answers = this.get('controllers.answer');
var answer = answers.content.filter(function(ans) {
if(ans.get('question.id') == questionId) { return true }
}
var choice = answer.mapProperty('choice.id');
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
});
And here a working fiddle.
Hope it helps.
I'm having issues syncing JSON data received from the server with my views after a fetch.
I do not have a collection of "mainmodel", because I'm only working with one "mainmodel" at a time but numerous "mymodel", anyhow, the structure follows:
var mymodel = Backbone.Model.extend({
defaults: {k1:"",
k2:"",
k3:""}
});
var collection = Backbone.Collection.extend({model:mymodel,});
var mainmodel = Backbone.Model.extend({
defaults: {v1:"",
v2:"",
v3:"",
v4:new collection()
});
I create the nested views for "mymodel" from a render function in a parent view. This works..., only when I'm working with a new model.
// My ParentView render function
render: function() {
for (var i = 0; i < this.model.v4.length;i++) {
var view = new MyModelView({model:this.model.v4.at(i)});
this.$el.append($(view.render().el));
}
this.$el.append(this.template(this.model.toJSON()));
return this;
},
// The MyModelView render function below
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
Now, the above works if I open my application and create models from there. However, If I open my app and supply an id, I make a fetch to the server, retrieve the data, and create a new ParentView I end up getting an error that says "this.model.v4.at not a function". Ugh.
So now, if I change the FIRST render function to be, changing the at(i) to [i]
var view = new MyModelView({model:this.model.v4[i]});
And change the second render function, removing toJSON, to be:
this.$el.html(this.template(this.model));
It renders. But I still can't move around views without errors. No surprise.
I have used console.log(JSON.stringify(this.model)); as they arrive into the parentView and MyModelView. The JSON returned looks like this, whether fetched or created.
{"v1":"val1",
"v2":"val2,
"v3":"val3",
"v4":[{"k1":"key1","k2":"key2","k3","key"}, { ... }, { ... }]
}
The JSON data structures appear to be identical. I thought the JSON format was incorrect, so I tried using JSON.parse before handing the model to the view, but that didn't work. Maybe I'm way off, but I originally thought I had a JSON formatting issue, but now I don't know. The server is returning content as 'application/json'.
Edit: The JSON values for v1,v2,v3 render correctly.
Any ideas?
You have two problems: one you know about and one you don't.
The problem you know about is that your mainmodel won't automatically convert your v4 JSON to a collection so you end up with an array where you're expecting a collection. You can fix this by adding a parse to your mainmodel:
parse: function(response) {
if(response.v4)
response.v4 = new collection(response.v4);
return response;
}
The problem you don't know about is that your defaults in mainmodel has a hidden reference sharing problem:
var mainmodel = Backbone.Model.extend({
defaults: {
//...
v4: new collection()
}
});
Anything you define in the Backbone.Model.extend object ends up on your model's prototype so the entire defaults object is shared by all instances of your model. Also, Backbone will do a shallow copy of defaults into your new models. So if you m1 = new mainmodel() and m2 = new mainmodel(), then m1 and m2 will have exactly the same v4 attribute. You can solve this by using a function for defaults:
var mainmodel = Backbone.Model.extend({
defaults: function() {
return {
v1: '',
v2: '',
v3: '',
v4: new collection()
};
}
});