backbone paginator multi model in a view - javascript

I have a shopping cart app made with Backbone.Paginator.Fluenced and forked with this example; https://github.com/msurguy/laravel-backbone-pagination
I made some small changes;
when you click over an item link, it opens a bootstrap modal window.
The code is below.
app.views.ItemView = Backbone.View.extend({
tagName: 'div',
className: 'col-sm-4 col-lg-4 col-md-4',
template: _.template($('#ProductItemTemplate').html()),
events: {
'click a.openModal': 'openModal'
},
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('remove', this.remove, this);
},
render : function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
openModal : function () {
var view = new app.views.ModalView({model:this.model});
view.render();
}
});
and this is my ModalView to show product details in a modal window.
app.views.ModalView = Backbone.View.extend({
template: _.template($('#modal-bsbb').html()),
initialize: function() {
_.bind(this.render, this);
},
render: function () {
$('#myModalPop').modal({backdrop: 'static',keyboard: true});
$('#myModalPop').html(this.template({
'model':this.model.toJSON()
}));
return this;
}
});
Everything is fine for above codes.
I decided to optimize this code and wanted some improvements on this.
Firstly I am fetching all product data and send these data to modal windows.
I think i must send only main meta data and must fetch details from these window.
So i made a new Backbone Model and Collection;
app.models.ItemDetails = Backbone.Model.extend({});
app.collections.ItemDetails = Backbone.Collection.extend({
model: app.models.ItemDetails,
dataType: 'json',
url : "/api/item-details",
parse: function(response){
return response.data;
}
});
My api returns JSON :
{"data":{"id":8,"title":"Product 8","seo":"product-8","code":"p8","review":"Lorem30"}}
My problem is adding multiple models to ModalView;
I tried a lot of example and questions in blogs&forums couldnt find any solve.
I tried a lot of things ($.extend, to set model and model vs..)
to change ModalView and below codes are last position of them;
app.views.ModalView = Backbone.View.extend({
template: _.template($('#modal-bsbb').html()),
initialize: function() {
_.bind(this.render, this);
},
render: function () {
var itemDetails = new app.collections.ItemDetails(); // this is new line
var model2 = itemDetails.fetch(); // this is new line
$('#myModalPop').modal({backdrop: 'static',keyboard: true});
$('#myModalPop').html(this.template({
'model1':this.model.toJSON(),
'model2':model2.model // this is new line
}));
return this;
}
});
I want to add a second model to my underscore template. But cant!
Firstly when i run below codes on chrome developer console it gets an Object;
but couldnt convert as a new model or JSON.
var itemDetails = new app.collections.ItemDetails();
var model2 = itemDetails.fetch();
console.log(model2); // gets fetch data as an object
I am afraid I am confused about where the problem exactly is.
Sorry guys I am not a backbone expert and probably I am doing something wrong though I searched a lot about it on the forum. I read about it again and again but I could not solve the problem. Could you please help me. Thank you in advance.
SOLVE:
After searchs and by the help of below reply.
I solved my problem.
app.views.ModalView = Backbone.View.extend({
template: _.template($('#modal-bsbb').html()),
initialize: function() {
_.bind(this.render, this);
},
render: function () {
var _thisView = this;
var itemsDetails = new app.collections.ItemsDetails();
itemsDetails.fetch({
success:function(data){
$('#myModalPop').modal({backdrop: 'static',keyboard: true})
.html(_thisView.template({
'model1':_thisView.model.toJSON(),
'model2':data.at(0).toJSON()
}));
}});
}
});

Every request to server using backbone is async, it means that you will not have the returned data immediately after the request, maybe the server still processing the data.
To solve this problem you have 2 ways.
First Way: Callbacks
Inside your Model/Collection
GetSomeData:->
#fetch(
success:=(data)>
console.log data // the returned data from server will be avaiable.
)
Second way: Listen for an trigger.
This one it's more elegant using backbone because you don't write callbacks.
Inside Model
GetSomeData:->
#fecth()
Inside View
initialize:->
#model = new KindOfModel()
#model.on "sync", #render, #
backbone automatically will trigger some events for you, take a read here.
http://backbonejs.org/#Events
As you're already doing, you'll need to listen to some trigger on the collection too
var itemDetails = new app.collections.ItemDetails(); // this is new line
var model2 = itemDetails.fetch(); // here is the problem

Related

Backbone JS Button to open a new view, save values in form

Im new to backbone and I'm looking to a very simple 2 view configuration page usig backbone.
I have the following code;
define(
["backbone","...","..."],
function(Backbone, ... , ... ) {
var PopupView = Backbone.View.extend({
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this,arguments);
},
events: {
"click .save_conf_button": "save_conf",
},
render: function() {
this.el.innerHTML = this.get_popup_template();
return this;
},
save:conf: function save_conf() {
//get the field values from popup_template
//var items = jquery(....);
});
var ExampleView = Backbone.View.extend({
//Starting view
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this, arguments);
},
events: {
"click .setup_button": "trigger_setup", //Triggers final setup
"click .create_conf_button": "trigger_popup_setup", //This is the conf popup
},
render: function() {
this.el.innerHTML = this.get_start_html();
return this;
},
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
PopupView.render();
...
},
}); //End of exampleView
return ExampleView;
} // end of require asynch
); // end of require
E.g. The ExampleView is the starting view with a couple of fields and 2 buttons; create popup and save. Upon pressing the create_conf_button I want to render the popup view, however this does not seem to work as I expected. (Uncaught TypeError: PopupView.render is not a function)
I'm not sure how to proceed and additionally what the "best practice" is for generating these types of dialogs?
Additionally, keeping the values filled in on the previous page after returning from the popupview would be preferential.
Thanks for any help
try
new PopupView.render()
you have to create an instance to call the methods this way
#ashish is correct, you have to instantiate an instance of the PopupView before calling its render method. Currently, you have defined a blueprint for a view called PopupView, which will act as a constructor for newly created PopupView view instances. In order to use this defined view I would suggest storing it in ExampleView's render or initialize method:
// Example View's initialize method
initialize: function initialize() {
this.popUpView = new PopupView();
Backbone.View.prototype.initialize.apply(this, arguments);
},
then referencing it in your trigger_popup_setup function as follows:
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
this.popUpView.render();
...
},
As for storing state Backbone models are used for that :)
In general to nest subviews within a master view in Backbone you can do the following:
initialize : function () {
//...
},
render : function () {
this.$el.empty();
this.innerView1 = new Subview({options});
this.innerView2 = new Subview({options});
this.$('.inner-view-container')
.append(this.innerView1.el)
.append(this.innerView2.el);
}
In this example the master view is creating instances of it's subviews within its render method and attaching them to a corresponding DOM element.

Backbone.js firing other view's event

I'm working with an API and Backbone.js at the moment.
I have two views, both render to the same document element #viewContainer. Both of these views render a table with a couple strings to decribe them and a button that opens a form in a modal.
View 1
App.Views.TaskList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showTaskForm"
},
showTaskForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
render: function () {
changeActive($('#tasksLink'));
var template = _.template($("#taskList").html(), {});
$('#viewContainer').html(template);
// loop and render individual tasks.
this.collection.each(function (model) {
var variables = {
name: model.get('name'),
button: model.getButton()
};
var template = _.template($("#task").html(), variables);
$("#taskTable tbody").append(template);
});
},
collection: App.Collections.Tasks,
});
View 2
App.Views.ProcessList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showStartForm"
},
showStartForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
collection: App.Collections.Processes,
render: function () {
changeActive($('#processLink'));
var template = _.template($("#processList").html(), {});
$('#viewContainer').html(template);
this.collection.each(function (model) {
var variables = {
processId: model.get('id'),
processName: model.get('name'),
button: model.getButton()
};
var template = _.template($('#process').html(), variables);
$('#processList tbody').append(template);
});
} });
Neither of these views are rendered by default, both need to be activated by a button on the page and they over-write each other in the DOM. However, which ever view is rendered first, the click event of the buttons in that view are the ones that are always fired.
If there is any more information needed from me let me know and I will edit the question.
Be sure to call undelegateEvents() in the first view when you render the second.
Since you're listening for events on the same elements, essentially you attached two listeners for click events on the same button, and when you change your views you are not cleaning up these listeners.
Here's an article that talks about managing events on view change, which should be really helpful to you.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
As other posters have pointed out, you need to watch out for 'zombie' views (i.e. making sure you undelegate events). If you're building even a moderately complex app, you'll want something that can scale. I find this pattern useful:
var BaseView = Backbone.View.extend({
render: function () {
this.$el.html(this.template());
return this;
},
close: function () {
if (this.onClose) this.onClose();
this.undelegateEvents();
this.$el.off();
this.$el.remove();
}
});
Then whenever you build a view you can do:
var view = BaseView.extend({
//your code
//now the .close() method is available whenever you need to close
//a view (no more zombies!).
});

Old Backbone View Causing extra Event Triggers

OK, I've done some reading on this and I'm pretty sure I know what the problem relates to I Just don't know the best way to fix it. I've got the standard backbone router that sets me up with an item details view, then when I click on a button called "start" it creates a new view which takes me to a sort of a game that people can play with some buttons on the bottom that have "click" events attached. This second view is not called through the router but directly from the first view.
The problem is the second time someones goes back to the homescreen and does it again, this time there are two events attached to each button. The third time there are three events. Obviously the original views are still listening to these buttons. I've read about this and calling the Remove() method but is this what I need to do? If so where do I call remove? Relevant Code below:
1ST VIEW
window.GameDrillView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .start" : "startGameDrill",
},
startGameDrill: function () {
var start = $('#start').val();.
var stop = $('#stop').val();.
var StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
}
});
START VIEW
window.GameDrillStartView = Backbone.View.extend({
// declare variables
initialize: function () {
this.render();
},
events: {
"click .nextstage" : "nextstage", // 2ND TIME THROUGH GETS CALLED TWICE
},
nextstage: function () {
// Do some stuff //
this.render(); //Re-render
},
render: function () {
// Do some variables stuff
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)));..
return this;
}
});
When changing view you need to call undelegateEvents() method from the Backbone.View. It disable listening all the elements events mentioned in events { } block. Also if you need to destroy old view you can call remove() method of the view which will call undelegateEvents() internally.
update (example from official site)
var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new HelpView();
},
search: function(query, page) {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new SearchView();
}
});
An option is to create only one instance of the view:
if(_.isUndefined(this.StartView))
this.StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
else
this.StartView.render();
In the render method of GameDrillStartView add the empty method
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)))
In this way you won't add more event listeners but you'll update the page everytime the user presses the button.
You can manage the life cycle of StartView in GameDrillView since it seems like a better place to do so.
Got same trouble. Messy solution:
var current_view = false;
var prev_view = false;
var AppRouter = Backbone.Router.extend({
routes: {
"events/:id": "viewEvent",
}
});
var app_router = new AppRouter;
app_router.on('route:viewEvent', function (event_id) {
var _event = new Event({id:event_id});
current_view = new EventView({
model: _event,
});
});
//Will be called after route:viewEvent
app_router.on('route', function () {
if(prev_view) {
prev_view.undelegateEvents();
}
prev_view = current_view;
});
Not sure, how to make it without having current_view and prev_view out of router scope.

Backbone.js: How do you call a View's "method" from outside the View's scope (e.g: inside a model's validation handler)

Basically, I'm trying to do something like this:
Person = Backbone.Model.extend({
validate: { ... },
initialize: function(){
this.bind('error', ?......?); <== what do I put?
},
// I DON'T WANT TO CALL THIS ONE
handleError: function(){ }
});
ViewOne = Backbone.View.extend({
//I WANT TO CALL THIS ONE:
handleError: function(model, error){
//display inside segmented view using jQuery
};
});
I tried options.view.handleError but it doesn't work...
My main purpose: I want a specific View that created the model to handle the error, not have the model to globally handle it all. For example, I want View#1 to do an alert while I want View#2 to display in a div. I don't know if this is the right way of doing it. If not, I would be gladly accept your help.
Thank you.
UPDATE: here's my jsFiddle
http://jsfiddle.net/jancarlo000/87mAk/
Since Backbone 0.5.2 it is recommended to drop bindAll in favor of third argument to bind if you need to pass the context.
ViewOne = Backbone.View.extend({
initialize: function() {
this.model.on('error', this.handleError, this);
},
handleError: function(model, error) { /* ... */ }
});
...
var person = new Person();
var viewone = new ViewOne({model : person});
General note here is that Models should never know about their Views. Only Views should subscribe to Model events.
You have it backwards, the view should be binding to the model's events:
ViewOne = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'handleError');
this.model.bind('error', this.handleError);
},
handleError: function(model, error) { /* ... */ }
});

backbone.js model not firing events

I have a view which doesn't seem to want to render as the model's change event is not firing.
here's my model:
var LanguagePanelModel = Backbone.Model.extend({
name: "langpanel",
url: "/setlocale",
initialize: function(){
console.log("langselect initing")
}
})
here's my view:
var LanguagePanelView = Backbone.View.extend({
tagName: "div",
className: "langselect",
render: function(){
this.el.innerHTML = this.model.get("content");
console.log("render",this.model.get(0))
return this;
},
initialize : function(options) {
console.log("initializing",this.model)
_.bindAll(this, "render");
this.model.bind('change', this.render);
this.model.fetch(this.model.url);
}
});
here's how I instantiate them:
if(some stuff here)
{
lsm = new LanguagePanelModel();
lsv = new LanguagePanelView({model:lsm});
}
I get logs for the init but not for the render of the view?
Any ideas?
I guess it's about setting the attributes of the model - name is not a standard attribute and the way you've defined it, it seems to be accessible directly by using model.name and backbone doesn't allow that AFAIK. Here are the changes that work :) You can see the associated fiddle with it too :)
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "Some test content" //just 'cause there wasn't anything to fetch from the server
},
url: "/setlocale",
initialize: function(){
console.log("langselect initing"); //does get logged
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'), //added here directly so that content can be seen in the actual div
initialize : function(options) {
console.log("initializing",this.model);
_.bindAll(this, "render");
this.render(); //calling directly since 'change' won't be triggered
this.model.bind('change', this.render);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c); //for UI visibility
console.log("render",this.model.get(0)); //does get logged :)
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
}); //end ready
UPDATE:
You don't need to manually trigger the change event - think of it as bad practice. Here's what the backbone documentation says (note: fetch also triggers change!)
Fetch
model.fetch([options])
Resets the model's state from the server.
Useful if the model has never been populated with data, or if you'd
like to ensure that you have the latest server state. A "change" event
will be triggered if the server's state differs from the current
attributes. Accepts success and error callbacks in the options hash,
which are passed (model, response) as arguments.
So, if the value fetched from the server is different from the defaults the change event will be fired so you needn't do it yourself. If you really wish to have such an event then you can use the trigger approach but custom name it since it's specific to your application. You are basically trying to overload the event so to speak. Totally fine, but just a thought.
Change
model.change()
Manually trigger the "change" event. If you've been
passing {silent: true} to the set function in order to aggregate rapid
changes to a model, you'll want to call model.change() when you're all
finished.
The change event is to be manually triggered only if you've been suppressing the event by passing silent:true as an argument to the set method of the model.
You may also want to look at 'has changed' and other events from the backbone doc.
EDIT Forgot to add the updated fiddle for the above example - you can see that the alert box pops up twice when the model is changed by explicitly calling set - the same would happen on fetching too. And hence the comment on the fact that you "may not" need to trigger 'change' manually as you are doing :)
The issue was resolved my adding
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "no content",
rawdata: "no data"
},
events:{
//"refresh" : "parse"
},
url: "/setlocale",
initialize: function(){
log("langselect initing");
//this.fetch()
},
parse: function(response) {
this.rawdata = response;
// ... do some stuff
this.trigger('change',this) //<-- this is what was necessary
}
})
You don't need attributes to be predefined unlike PhD suggested. You need to pass the context to 'bind' - this.model.bind('change', this.render, this);
See working fiddle at http://jsfiddle.net/7LzTt/ or code below:
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
url: "/setlocale",
initialize: function(){
console.log("langselect initing");
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'),
initialize : function(options) {
console.log("initializing",this.model);
// _.bindAll(this, "render");
//this.render();
this.model.bind('change', this.render, this);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c);
console.log("render",this.model.get(0));
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
lpm.set({content:"hello"});
}); //end ready

Categories