So I have two templates, one is the parent template, and the other is a template that has the subviews in it. My first parent view extends backbone's view and its render method looks like
render: function () {
this.$el.append(templates["template-parent-test"](this.model));
return this;
}
This parent view has a button on it. The button right now I use to populate my view. It basically does this:
populateView: function () {
// create some dummy test data to match the web service
_.each(myModel, function (theModel) {
var testView = new childView({ model: theModel });
this.$('div-in-parent-view').append(testView.render().$el);
});
}
This backbone view extends the Backbone.View and its render method looks like:
render: function () {
console.log("rendering child view");
this.$el.html(this.template({ data: this.model.toJSON() }));
return this;
}
So this works. But I don't want to populate the parent view on a button press. I want to populate it when I show it for the first time and have the button do what its actually supposed to do. I would think that I could just call this.populateView() from my render function in the parent view, but nothing actually gets rendered. But if I do this.populateView() in my button event, it gets rendered. Why is there a difference here? Thanks.
Try adding this as a third parameter to each. That sets the context. When you don't specify the context, this will be window. And since your html is not yet on the page during the parent view's render, it wont find this.$('#div-in-parent-view') in order to add the html to it.
Related
I am relatively new to Backbone and I am running into this problem.
I am using Backbone with DustJS
My template looks something like this - index.dust
<div id="parentView">
<div class="section">
{>"app/inc/responseMessage" /}
<div class="userDetails">
{! A button to get user details !}
</div>
</div>
</div>
This is my partial below - responseMessage.dust
<div id="responseMessage">
{#eq key="{data.success}" value="true"}
<div class="alert alert-success success" role="alert">success</div>
{/eq}
</div>
My JS looks like this
initialize: function() {
this.responseMessageView = this.responseMessageView ||
new ResponseMessageView({
model: new Backbone.Model()
}); // View is for the partial
this.model = this.model || new Backbone.Model(); //View for the whole page
},
Below function is called when an event occurs and it does a POST and returns successfully.
primaryViewEventTrigger: function(event){
//Button click on `index.dust` triggers this event and does a POST event to the backend
this.listenToOnce(this.model, 'sync', this.primaryViewSuccess);//this will render the whole view.
this.listenToOnce(this.model, 'error', this.primaryViewError);
this.model.save({data: {x:'123'}});
}
responseViewEventTrigger: function(event){
//Button click on `responseMessage.dust` triggers this event and does a POST event to the backend
this.listenToOnce(this.responseMessageView.model, 'sync', this.responseViewSuccess);//it should only render the partial view - responseMessage.dust
this.listenToOnce(this.responseMessageView.model, 'error', this.primaryViewError);
this.responseMessageView.model.save({data: {x:'123'}});
}
primaryViewSuccess: function(model,response){
this.model.set('data', response.data);
this.render();
}
responseViewSuccess: function(model,response){
this.responseMessageView.model.set('data', response.data);
console.log(this.responseMessageView.model);
this.responseMessageView.render(); // Does not work in some cases
}
My implementations of the callback function
exports.sendEmail = function sendEmail(req, res){
req.model.data.success = true;
responseRender.handleResponse(null, req, res);
};
this.model belongs to the model of the whole page. Whereas this.responseMessageView.model is the model for the partial.
Question: This works perfectly fine in most of the cases. There is one case where it does not render the partial with the latest model values. When I click on the button on index.dust and primaryViewSuccess is executed. After which I click on another button and trigger responseViewEventTrigger. It does the POST successfully and it comes to responseViewSuccess and stores it in the model too. But it does not show it on the frontend. data.success is still not true whereas console.log(this.responseMessageView.model) show that attributes->data->success = true
But the same behavior when I refresh the page it all works perfect. Its just that when primaryViewSuccess is called and then responseViewSuccess its not taking the latest model changes. In other words model is being updated but the DOM remains the same.
What am I missing here? Thanks for your time!
You're hitting the classic Backbone render-with-subviews gotcha: When you render the index view, you are replacing all of the DOM nodes. That includes the DOM node that your responseMessageView instance is tied to. If you inspect the responseMessageView.el you'll actually see that it has been updated with the new model data, however the element isn't attached to the index's DOM tree anymore.
var Parent = Backbone.View.extend({
template: _.template(''),
render: function() {
this.$el.html(this.template());
},
initialize: function() {
this.render();
this.child = new Child();
this.$el.append(child.el);
}
});
Here when you manually call render, child.el will no longer be in the
parent.el (you can check using the inspector).
The simplest fix here is to call child.setElement after the parent renders with the newly rendered div#responseMessage element. The better fix is to detach the responseMessageView's element beforehand, and reattach after:
var Parent = Backbone.View.extend({
render: function() {
this.responseMessageView.$el.detach();
this.$el.html(this.template(this.model.toJSON()));
this.$el.find('#responseMessage').replaceWith(this.responseMessageView.el);
}
});
Try to use success callback and let me know I think your problem may come from here:
this.model.save( {att1 : "value"}, {success :handler1, error: handler2});
Also are you sure you want to use listenToOnce ? instead of listenTo ??
Try doing this.delegateEvents() after your render (http://backbonejs.org/#View-delegateEvents).
I've been trying to learn Backbone, and I'm developing an app now. But I have a problem with a view's events: App.views.ChannelView should have a click event, but it is not firing.
Here's the code:
http://pastebin.com/GgvVHvtj
Everything get rendered fine, but events won't fire. Setting the el property in the view will work, but I can't use it, and I've seen on Backbone's todo tutorial that it is possible.
How do I make events fire without a defined el property?
You must define the el element to be an existing element in your DOM. If you do not define it, fine, it will default to a div, but when you render the view, the html generated must be appended/prepended whatever, you get the point, to an existing DOM element.
Events are scoped to the view, so something's wrong with your scope. From the code you provided I can't reproduce the problem, so if you might, please provide a live example on jsfiddle/jsbin etc in order to fully understand the issue.
Demo ( in order to demonstrate the view render )
var App = {
collections: {},
models: {},
views: {},
};
App.models.Channel = Backbone.Model.extend({
defaults: {
name: '#jucaSaoBoizinhos'
}
});
App.views.ChannelView = Backbone.View.extend({
el:$('#PlaceHolder'),
events: {
"click .channel": "myhandler"
},
render: function() {
this.$el.html('<div class="channel"><button>' + this.model.get('name') + '</button></div>');
return this;
},
myhandler: function(e) {
alert(e);
console.log(this.model.get('name'));
},
});
var chView = new App.views.ChannelView({model: new App.models.Channel()});
//console.log(chView.render().el) //prints div#PlaceHolder
//without the el specified in the view it would print a div container
//but i would have to render the view into an existing DOM element
//like this
//$('#PlaceHolder').html(chView.render().el)
chView.render()
Can you try doing a
events: {
"all": "log"
}
log: function(e) {
console.log e;
}
That should log out every event that's getting fired. I find it super helpful when troubleshooting.
backbone view events can work without dom element specified. If you can't use any element at the view createion (initialization) moment, then you can use it's 'setElement' method, to attach your view to specified dom element. Here is description.
Be the way your view render method will not work also without specified 'el'.
I have a view:
App.Views.Rebill = App.Views.baseView.extend({
view: 'requests._rebill_line',
tag: 'div',
className: 'row row-spaced',
render: function() {
var self = this;
App.get_view(self.view).done(function(data){
var view = Mustache.render(data, {
text: self.model.get('text'),
cost: self.model.get('cost')
});
self.$el.append(view);
var $selectVat = self.$el.find('select[name="vat"]');
var vatPicker = new App.Views.VatPicker({
model: self.model,
el: $selectVat
});
vatPicker.render();
});
return self;
}
});
this view is created in the parent:
addRebillLine: function(rebillModel){
var self = this;
var rebillView = new App.Views.Rebill({
model: rebillModel
});
self.$el.after(rebillView.render().$el);
},
I have linked this to a button, which creates an empty model, and calls the addRebillLine function. This works fine, the new Rebill view appears in the DOM when I click the button.
However, on rendering the parent view, I run through a json array, create a model with each line, and call the addRebillLine function with that model. This runs, but the Rebill views are not added to the DOM.
Further up, the parent view is itself a child view, and is attached to its parent like so:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
The parent and grandparent views are rendered synchronously, and the child view asynchronously (App.get_view() is basically a call to $.ajax, hence the .done())
The wierd thing is that I do the same thing with the App.Views.VatPicker view, in several other places, and that works just fine. The only difference is that I pass an element to attach to into the VatPicker view. But if I pass the parent $el element in, and run this.$parent.after(self.$el) in my done() callback, that doesn't work either.
When you call this line:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
it assumes your render code is synchronous (as it should be) , but you render code is not.
When render return the $el is not set yet, this is a problem in your design.
You should solve this design issue by making your templates available when the view needs them. Don't invent the wheel here, take a look at the TodoMVC backbone example.
If you want your templates to be loaded aysnc, use requirejs.
I would like to know if it possible to extend in some way the mechanism Marionette Layouts are based on creating a sort of stack like navigation.
Marionette behaviour.
Before a region show()'s a view it calls close() on the currently displayed view. close() acts as the view's destructor, unbinding all events, rendering it useless and allowing the garbage collector to dispose of it.
My scenario.
Suppose I have a sort of navigation mechanism where a Layout acts as controller and first displays an ItemView called A, then a click somewhere allows to switch to ItemView B. At this point, an action on B (like for example a tap on a back button) allows to return to A without recreating it.
How is it possible to achieve the previous scenario without creating again A and maintaning its state?
For iOS people, I would like to mimic a sort of UINavigationController.
Any advice?
EDIT
My goal is to restore a prev cached view with its state without creating it again.
My scenario is the following. I have a layout with two regions: A e B.
I do a click somehere within A and A and B are closed to show C and D. Now a back click would restore A and B with their states. Events, models, etc...but since views are closed events are removed.
Use a backbone router to listen to URL change events. Setup routes for each of your views and then have the router call the layout to change the view it's displaying in response to each route. The user could click back or forward any number of times and the app responds accordingly and displays the correct view. Your router might look like:
var Router = Backbone.router.extend({
routes: {
'my/route/itemViewA': 'showItemViewA',
'my/route/itemViewB': 'showItemViewB'
},
showItemViewA: function () {
layout.showItemView('a');
},
showItemViewB: function () {
layout.showItemView('b');
}
});
Your layout might look something like this:
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: 'my-region-jquery-selector'
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (view) {
this.someRegion.show(this.views[view]);
// You might want to do some other stuff here
// such as call delegateEvents to keep listening
// to models or collections etc. The current view
// will be closed but it won't be garbage collected
// as it's attached to this layout.
}
});
The method of communication between the router and the layout doesn't have to be a direct call. You could trigger further application-wide events or do anything else you can think of. The router above is very basic but gets the job done. You could create a more intelligent router to use a single route with parameters to determine dynamically which itemView to show.
Every time the user does something that requires changing views, you can update the browser's history by using router.navigate('my/route/itemViewB', {trigger: true});. Also, if you set up your app to only render on history change events then you don't need to set up two mechanisms for rending each view.
I use this pattern in my own apps and it works very well.
#Simon's answer is headed in the correct direction. However, the only way to stop Marionette from closing views is to modify a bit of it's Region code.
var NoCloseRegion = Marionette.Region.extend({
open: function(view) {
// Preserve the currentView's events/elements
if (this.currentView) { this.currentView.$el.detach(); }
// Append the new view's el
this.$el.append(view.el);
}
});
The, when be sure to specify our new Region class when creating the Layout view
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: {
selector: 'my-region-jquery-selector',
regionType: NoCloseRegion
},
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (name) {
// Don't `show`, because that'll call `close` on the view
var view = this.views[name];
this.someRegion.open(view)
this.someRegion.attachView(view)
}
});
Now, instead of calling show which closes the old view, renders the new, and attaches it to the region (and triggers a few events), we can detach the old view, attach the new, and open it.
I am trying to use templating to render a view initially, but to update the view on subsequent calls when it is part of the document.
My app's page looks a little something like
<body>
<!-- ... -->
<div id="view_placeholder"/>
<!-- ... -->
</body>
And in pseudo-code I want to do something like so
Backbone.View.extend({
// ...
render: function() {
if (this.el *IS NOT A CHILD OF document*) {
// render the contents from the template
} else {
// update the content visibility based on the model
}
},
// ...
});
The reason for this is that the template contains quite a lot of nodes and regenerating it for every change is not practicable.
I have explored some of the data-binding libraries, e.g. rivets.js but they are a poor fit to the template:model relation.
One thing I noticed is that this.el.parentNode==null before I add it to the document, but I am not sure that this is a definitive test, and in any case if I wrap this view within another, then that test becomes less useful (or maybe I am being overly cautious as once within another view's el I have rendered my sub-template anyway.
Another option I can see is to use a field to track the rendered status, e.g.
Backbone.View.extend({
//
templateRendered:false,
// ...
render: function() {
if (!this.templateRendered) {
// render the contents from the template
this.templateRendered = true;
} else {
// update the content visibility based on the model
}
},
// ...
});
but that feels hacky to me.
So my question is:
What is the best way to track the fact that I have rendered the template fully and therefore only need to tweak the rendered template rather than re-render it (and re-insert all the sub-views)?
I think the idiomatic backbone approach is to only call a full render() on your view when you want a full render, and use model change event bindings to call sub-render functions that render smaller portions of the view.
var AddressView = Backbone.View.extend({
initialize: function (options) {
Backbone.view.prototype.initialize.call(this, options);
_.bindAll(this)
options.model.on('change:name', this.renderName);
options.model.on('change:street', this.renderStreet);
options.model.on('change:zipCode', this.renderZipCode);
},
renderName: function (model) {
this.$el.find("#name").text(model.get("name"));
},
renderZipCode: function (model) {
this.$el.find("#zipcode").text(model.get("zipCode"));
},
renderStreet: function (model) {
this.$el.find("#stree").text(model.get("street"));
},
render: function () {
//Populate this.el with initial template, subviews, etc
//assume this.template is a template function that can render the main HTML
this.$el.html(this.template(model));
this.renderName(this.model);
this.renderZipCode(this.model);
this.renderStreet(this.model);
return this;
}
});
Code as above is undoubtedly tedious. I would reconsider knockback.js or rivets.js, personally, but I believe the pattern above is the canonical vanilla backbone.js approach.
I would avoid rendering a view until its element is about to be inserted. In any case, you can find out by checking the parent of the view's element, i.e.
this.$el.parent()
should be empty if the this.$el is not part of your document.