Backbone - expanded views and zombies - backbone.js

I render a collection of backbone models in a list view, where each model has its own listItemView such as.
var els = [];
_.each(this.collection.models, function(model){
var view = new TB_BB.RequestItemView({model : model});
els.push(view.render().el);
});
$('#request-list').append(els);
Each of the ItemsView can be expanded on a click event (where a call to the server is made) such as.
showDetails : function() {
var m = new TB_BB.RequestDetails({id : this.model.get('id')});
var outerthis = this;
m.fetch({success : function() {
var det = new TB_BB.RequestDetailsView({model : m, el : outerthis.el, current : outerthis, template : '#request-expanded-template'});
det.render();
}});
}
So the logic is that this expanded view opens in the el of the current item. You may have noticed that I am passing a reference to the current view (outerthis) this was to avoid me having zombies of this view when closing expanding view.
So in the expanded view we have a 'hide' method which should hide the expanded view and show the original element such as (where current is the reference to the non-expanded view).
hideDetails : function() {
$(this.el).empty();
this.options.current.render();
}
I'm pretty sure this is not the best way of doing this - but not sure what would be the best way? In this case there are no zombies when calling the hidedetails (as its referencing the original view). However I am guessing that each time the 'showDetails' view is called and closed a new zombie exists? Could anyone enlighten what would be a better way of having expanded views of a list?

I've had a similar case in the past where I had to render a tree type structure, with list items opening to show sub-lists and closing again. The way I implemented the solution was to have the something like this as the html structure:
<div class="list-item">
<div class="handle closed" />
<span><%= title %></span>
<div class="sub-list" />
</div>
So each list item was rendered with an empty element for the sub view. Then in the click handler I would check if this list-item already has created a view for the sub-list, if yes just show the sub-list element; if not, create sub list view, which renders ajax-loader.gif and call collection.fetch to fetch data.
So my recommendation is to use separate dom elements, create subviews lazily and just hide the dom element when you close something, keeping a reference to the view.

Related

How do I design MarionetteJS ItemView to properly show loading message?

First off - I am a MarionetteJS noob.
I am having trouble making an ItemView display a loading message or throbber while it is being fetched. This is especially problematic when this ItemView is being displayed from a deep link in Backbone's history (i.e. the ItemView is the first page of the app being displayed since the user linked directly to it). I want to indicate that the page is loading (fetching), preferably with a simple view, and then show the real templated view with the fetched model.
I have seen other answers on SO like Marionette.async, (which has been deprecated) and changing the template during ItemView.initalize().
Anybody (Derrick?) got any suggestions or best practices here?
UPDATE:
I am getting the model from the collection using collection.get(id), not using model.fetch() directly.
After thinking about this, the real question is where should this be implemented:
I could change my controller to see if the model exists in the collection (and if the collection is loaded) and decide which view to show accordingly. this seems like a lot of boilerplate everywhere since this could happen with any ItemView and any controller.
I could change my ItemView initialize to test for existence of the model (and a loaded collection), but same comment here: every ItemView could have this problem.
UPDATE 2:
This is what I ended up with, in case anybody else want this solution:
app.ModelLayout = Backbone.Marionette.Layout.extend({
constructor: function(){
var args = Array.prototype.slice.apply(arguments);
Backbone.Marionette.Layout.prototype.constructor.apply(this, args);
// we want to know when the collection is loaded or changed significantly
this.listenTo(this.collection, "reset sync", this.resetHandler);
},
resetHandler: function () {
// whenever the collection is reset/sync'ed, we try to render the selected model
if (this.collection.length) {
// when there is no model, check to see if the collection is loaded and then try to get the
// specified id to render into this view
this.model = this.collection.get(this.id);
}
this.render();
},
getTemplate: function(){
// getTemplate will be called during render() processing.
// return a template based on state of collection, and state of model
if (this.model){
// normal case: we have a valid model, return the normal template
return this.template;
} else if (this.collection && this.collection.isSyncing) {
// collection is still syncing, tell the user that it is Loading
return this.loadingView;
} else {
// we're not syncing and we don't have a model, therefore, not found
return this.emptyView;
}
}
});
And here is how to use it:
// display a single model on a page
app.Access.Layout.CardLayout = app.ModelLayout.extend({
regions: {
detailsRegion:"#detailsRegion",
eventsRegion:"#eventsRegion"
},
template:"CardLayout", // this is the normal template with a loaded model
loadingView:"LoadingView", // this is a template to show while loading the collection
emptyView:"PageNotFoundView", // this is a template to show when the model is not found
onRender : function() {
this.detailsRegion.show( blah );
this.eventsRegion.show( blah );
}
});
thanks!
For the ItemView
I think you can add a spinner in your initialize function, I really like spin.js http://fgnass.github.io/spin.js/ because its pretty easy and simple to use, and you can hide the spinner in the onRender function of the Itemview
For The CollectionView
in the CollectionView you could handle it like this....
Take a look at the solution that Derick posted..
https://github.com/marionettejs/backbone.marionette/wiki/Displaying-A-%22loading-...%22-Message-For-A-Collection-Or-Composite-View
I'd suggest using jQuery deferreds:
Start fetching your data, and store the return value (which is a jQuery promise)
Instanciate your content view
Show your loading view
When the promise is done, show the view containing the content
I've talked about implementing this technique on my blog:
http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/
The issue with the solution linked by Rayweb_on, is that your loading view will be displayed any time your collection is empty (i.e. not just when it's being fetched). Besides, the ItemView doesn't have an emptyView attribute, so it won't be applicable to your case anyway.
Update:
Based on your updated question, you should still be able to apply the concept by dynamically specifying which template to use: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#change-which-template-is-rendered-for-a-view
Then, when/if the data has been fetched successfully, trigger a rerender in the view.

Append backbone EL twice in Ajax Success Not Working

When I try to append my backbone view to two different places at the same time in my success method only the second appending works. Do you know why?
$(content).prepend(this.$el.append(this.template({ data: data })));
$(chat_window).prepend(this.$el.append(this.template({ data: data })));
Each DOM node can have exactly 0 or 1 parent nodes, never more than 1. If you append a node somewhere, it gets removed from it's current parent and then appended to the new parent. What you need here is 2 distinct view instances each with it's own element.
el corresponds to one html element that a backbone view generates. Into that html element you can append more html weather it be another backbone view or a rendered template.
Hence in your case if the el is attached twice it finally stays where it was attached last to the dom tree. If you want attach in multiple places then I guess you should instantiate the backboneview twice.
In my opinion, the view you're talking about should not know about its distant parents or cousins but rather should trigger an event "I have new content" and then the interested views can act upon this the way they want.
That being said there is a difference between a view and its html representation(s), you could design your app so that you get 2 places in the html where you put ".new-content-holder" and pass this selector as the el of your view upon creation. Then the 2 places will be updated at the same time without you explicitly programming it. I sometimes use this technique for example when I want a paginator for a long list to be displayed over and under the list.
Some html :
<div class="content">
<p>Recent comments</>
<ul class="new-content-holder"></ul>
</div>
<div class="chat-room">
Live feed
<ul class="new-content-holder">
<li>a chat message</li>
<li>another chat message</li>
</ul>
</div>
And a view
....
var MessageView = Backbone.View.extend({
template: _.template('<li class="chat-message"><%= message %></li>'),
prependData: function(data){
this.$el.prepend(this.template(data))
},
onMessage: function(message) {
this.prependData({message: message.data})
}
});
....
//And in a super controller of sorts :
var messageView = new MessageView(el: '.new-message-holder')
Again, this is not a very good separation of concerns...but I hope that helps.
I agree with #Peter Lyons, You can not inject the same node into two elements. Ultimately, the node will move to new element. One of the ways is to get HTML from the element you want to inject and inject the same HTML twice. Since html is a string and not a dom element. You can add it as many times and inside as many elements.
Try this one:
var html = this.$el.append(this.template({ data: data })).html();
$(content).prepend(html);
$(chat_window).prepend(html);
I hope you are not using id's on elements inside your template.
PS: I don't know your use case exactly.

Backbone.marionnette - Rebinding events vs creating new view

I have a Layout that has several tabs. Clicking one of these tabs will show the appropriate composite view in the page's content region. After navigating back and forth between different tabs I noticed that the composite views have lost their native bindings to render on collection reset and model changes.
Is there a way I should be rebinding the events being used in _initialEvents of a composite view when showing a view for a second time, or should I be creating a new composite view every I show a tab?
Currently I am creating all my views in initialize of my Layout and then using show with the view when a tab is clicked.
initialize: function(){
_.bindAll(this);
// Tabs
this.places_page = new Places_Layout();
},
show_places_page: function(){
this.content.show(this.places_page);
this.places_page.delegateEvents();
},
You don not have to create a Layout/Item/Composite/Collection view each time you switch from tab to tab, on the contrary you can save the content in a variable just the way you are doing, the problem you have is that the variable is being re-declared each time you want to render the content.
The solution is that you have to verify if that variable (this.places_page) is declared if not append it to the view so when you call it more times it will be holding the same layout view without any problem, just note that when you render the main view (the one holding the regions) the nested child views(in regions) will be lost until new navegation through them.
initialize: function(){
_.bindAll(this);
// You can asign a diferent variable for each view so when you call show_places_page it will render with the same view.
if (!this.places_page){
this.places_page = new Places_Layout();
}
// other tab
if (!this.other_page){
this.other_page = new OtherPage_Layout();
}
},
show_places_page: function(){
this.content.show(this.places_page);
this.places_page.delegateEvents();
},
This does not sound like the best approach to me.
You should use the layout's region managers to show views without needing functions like you have defined.
I would go for this approach
var view = new CustomView();
layout.content.show(view);`
then later on:
var newView = new SecondCustomView();
layout.content.show(newView);
If you want to continue down the road that you are on then you would probably be best to use this approach:
initialize: function () {
_.bindAll(this);
},
show_places_page: function () {
var placesLayout = new Places_Layout();
this.content.show(placesLayout);
}
Does that make sense?
Its hard to suggest the best course of action without seeing more structure around this.
Is there a reason that you are creating the views in initialize?
Marionette(v.1) onwords uses Backbone.BabySitter to manage child views .
In your case you do the same.
Just create a containter to store all tab view. Later query the container to return the view you need to display.
this.tabViewsContainer = new Backbone.ChildViewContainer();
this.tabViewContainer.add(new CustomView(),'tab1');
this.tabViewContainer.add(new SecondCustomView(),'tab2');
To Later Show the view just do this
var custv = container.findByCustom("tab1");
this.content.show(custv);
In close method your layout view successfully close all view in container
this.tabViewsContainer.each(function(view){view.close()});
You should not create all the views inside the initialize as this will cause you memory leaks that's why you should do dynamic creation of the views. Also I would suggest create a common function for showing a view in your content region to increase the code re-usability. I would suggest you something like following solution:
//define the regions of your layout view
regions: {
content: '#content'
},
//Maintain a config for the tab content view classes.
contentViews: {
tab1: Tab1View,
tab2: Tab2View,
tab3: Tab3View
},
//keeps all the view instances
viewInstances: {},
/*
* show tab function is called when you click a tab item.
* Consider each tab has a attribute for tab name.
* For example HTML of your one tab is like:
* <div data-tab-name="tab_name">Tab <tab_name></div>
*/
showTab: function (e) {
var tabName = $(e.currentTarget).attr("data-tab-name");
/*
* code for showing selected tab goes here...
*/
//check and create the instance for the content view
if (!this.viewInstances[tabName]) {
this.viewInstances[tabName] = new this.contentViews[tabName]();
}
//Then here you are actually showing the content view
this.content.show(this.viewInstances[tabName]);
this.viewInstances[tabName].delegateEvents(); //this is to rebind events to the view.
}

how to handle multiple views within a single view with backbonejs

I'm new to Backbone and I don't fully understand it yet, and I've come across a situation I can't find any documentation on. What I if I have a view that contains multiple views? For example, I have a view called StackView. The purpose of this view is to neatly lay out a set of cards. It manages the animation of adding, removing, and adjusting cards in the stack. Each card is a CardView. How would I handle this? I've seen people talk about views within views by simple creating a variable in the view and assigning the View instance to that variable. Should I just be adding an array of CardViews in a variable of a StackView?
That's what I do, and it works well. Here's a snippet of a View I use in an application. I've re-written it back into regular javascript from my coffeescript, so I apologize for any typos:
render: function() {
var _this = this;
this.$el.html(this.template());
this.listItemViews = [];
// for each model in the collection, create a new sub-view and add
// it to the parent view
this.collection.each(function(model){
var view = new App.Views.Projects.ListItem({model:model}); // create the new sub-view
_this.listItemViews.push(view); // add it to the array
_this.$('#project-table tbody').append(view.render().$el); // append its rendered element to the parent view's DOM
});
return this;
}
This lets my Table view maintain a reference to all the listItemView views.
Of course, if you do this, you should make sure to properly remove these child views and unbind any events when you remove the parent view.

Delegating events

I've got a backbone view for an entire collection (a list of "clickable" categories). Can I delegate events on each item of the view so that I can find which category has been clicked?
Here's a post that might help. Basically you use a data-* attribute in the item view to store and then retrieve the id of item clicked:
Backbone.js: Getting The Model For A Clicked Element
If you'd rather go directly to code, here's the jsFiddle that's used in the post to demonstrate. Hope that helps.
I have no answer for your question (no, I think), but would like to share my approach: a general collection view component, which renders a collection using other view. It can be as simple as in the example below or more sophisticated (listening add/remove/reset events and react accordingly).
var CollectionView = Backbone.View.extend({
render : function() {
this.options.collection.each(function(model) {
this.$el.append((new this.options.view({model : model})).el);
}, this);
}
})

Resources