Can Marionette.Layouts have dynamic content outside sub-regions? - backbone.js

I'm developing a single-page view for a resource that contains multiple nested resources. Using the following template, I can get the top-level attribute or collection to render, but not both:
h1= #name
ul
#decisions
class Happenator.Views.ShowHappening extends Backbone.Marionette.Layout
template: "happenings/show"
regions:
decisions: "#decisions"
initialize: ->
#decisionsView = new Happenator.Views.Decisions(collection: #model.get("decisions"))
# Uncomment to render #model.name, but lose the decisions
# #bindTo(#model, "change", #render)
onRender: ->
#decisions.show(#decisionsView)
Is there an accepted way to bind the top-level layout to re-render when data changes/arrives, or is all dynamic content support to go into sub-regions?

A Layout renders the DOM elements that the regions will manage. So, calling render on the layout again will forceably re-render the region's elements. The regions will see this and pick them up again, but this will have various other negative impacts on your app as you've noted.
The simple way to handle this is to either:
a) have individual change:attribute event handlers that only update the data that changed
b) use a data-binding solution to do this for you, such as https://github.com/theironcook/Backbone.ModelBinder
A third alternative would be to create a 2nd region within your Layout, and have an ItemView populate that region, with the model. Then you could re-render that view whenever the model changes:
h1= #name
ul
#decisions
class Happenator.Views.HeaderView extends Backbone.Marionette.ItemView
template: "happenings/model-template"
initialize: ->
#bindTo(#model, "change", #render)
class Happenator.Views.ShowHappening extends Backbone.Marionette.Layout
template: "happenings/show"
regions:
header: "#header",
decisions: "#decisions"
initialize: ->
#decisionsView = new Happenator.Views.Decisions(collection: #model.get("decisions"))
#headerView = new Happenator.Views.HeaderView(model: #model)
onRender: ->
#decisions.show(#decisionsView)
#header.show(#headerView)
If it were me, I'd go with option #3 and nest another itemview inside of the layout, for the model rendering. This would give you a lot more freedom and flexibility within that view, keeping it separated from the layout.

Related

How to determine if I should use a layout view Marionette?

I have skimmed through all the marionette articles on the layout view and I am not sure if there are advantages to using that versus how I have my app setup now, let me show you.
After building the components to my app I then created an app level view that handles the initialization and rending of all those views.
var Backbone = require('backbone'),
TeamsView = require('./teams'),
DataView = require('./teamData'),
LeaderView = require('./leader');
module.exports = appView = Backbone.View.extend({
el: '#wrap',
template: require('../../templates/app.hbs'),
initialize: function() {
window.App.views.teamsView = new TeamsView({ collection: window.App.data.teams });
window.App.views.dataView = new DataView({ collection: window.App.data.teams });
window.App.views.leaderView = new LeaderView({ collection: window.App.data.teams });
},
render: function() {
var teamsView = window.App.views.teamsView;
var dataView = window.App.views.dataView;
var leaderView = window.App.views.leaderView;
this.$el.find('#basketball .app').prepend(teamsView.render().el);
this.$el.find('#basketball .app').prepend(dataView.render().el);
this.$el.prepend(leaderView.render().el);
}
});
Then inside a controller I render the app view above.
This feels comfortable to me, but somewhere deep inside says it's wrong and I should be looking at layout views?
So my question more specifically is when putting the pieces together in an backbone application should I be looking into layout views or is creating a single app level view (like above) sufficient?
What you're doing is fine at the simplest level, though perhaps more manual than it needs to be. However, you may want a Layout View as things get more complex.
The value of a Layout View comes when you have a region in your App that you'd like to have contain sub-regions, but only during certain views. For example, you might have an index/list inside of your App's #mainRegion that just has a bunch of teams, in which case you could use a CollectionView (or CompositeView if you want to add some styling) along with the ItemView for each team.
However, say you click to edit one of the teams, and now you want the #mainRegion to show the edit page, which itself has some information about the team in #infoRegion and then an edit form in a #formRegion. These are regions that are specific to the edit page, and so you'd want to use a Layout View to manage them rather than delegating all the way up to the App level.
I hope this makes sense, and I'm happy to clarify if needed. You can also check out the documentation for a breakdown of when to use each view type: https://github.com/marionettejs/backbone.marionette/wiki/Use-cases-for-the-different-views

How can I run the same setup function on every view?

Many of the views in my application need to be "collapsible". To the user this means that you can click an arrow to collapse or expand the view's contents.
When creating a view I need to be able to easily say, "This view should be collapsible," and then run the appropriate setup code (which essentially means adding the .collapsible class to the view's wrapper and inserting a dom element that looks like this: <div class="toggle"></div>
Suggestions on ways to pull this off seamlessly? I'm currently using Backbone, Backbone.Marionette, and Underscore.
I do this with another application that doesn't use Backbone. In that application every action results in a page refresh, so I just use jQuery to look for all elements with the .collapsible class and do my setup that way.
EDIT:
I'm using Backbone.Marionette.CompositeView for these particular views, if that helps.
I've done similar thing in my project by extracting such functionality into mixins. There're different approaches to implementing mixins in Backbone. Take a look here or here
You can create parent view that extends from Marionettes compositeView and add your common functionallity there, and have your project views extend from this parent view.
var CollapsibleView = Backbone.Marionette.CompositeView.extends({
variable1: 1,
var2: true,
initialize : function() {
// your code here
},
helperfunction : function () {
// other helpful function
}
});
var MySpecificView = CollapsibleView.extends({
mySpecificFunction : function () {
// some specificView functionality
}
});
var myProjectView= new MySpecifcView();
myProjectView.helperfunction(); /// function from the parent
myProjectView.mySpecificFunction(); /// function from the specificView
/// you also have the functionality added on the initialization of the collpasibleView

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.
}

What is the best practice to use a jQuery Plugin with Backbone.js

I am using jQuery UI with a rails application using backbone.js. I want to make a draggable element? Where do I have to put this function :
$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
Is it on the view? After the render function? Because, the initialize function doesn't find my element, I think the DOM is not already created?
So, i did this :
class Myapp.Views.Tools extends Backbone.View
template: JST['pdfs/tools']
tagName: "div"
className: "pdf-tools"
events:
'click div.rect' : 'drawRect'
initialize: ->
#previewWrapper = $('.preview')
#count = 0;
#
render: ->
$(#el).html(#template())
#initColorPicker()
this
initColorPicker: ->
$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
drawRect: (event) =>
newElement = $('<div id="resizable" class="resizable"><div class="close">x</div><input type="text" name="text_' + #count++ + '" /></div>');
#previewWrapper.append(newElement);
newElement.draggable().resizable();
Is it good? Any recommendation?
I just had the same issue come up when integrating the timeago plugin into my rails/backbone.js app.
My solution was almost the same as yours, except that instead of applying the plugin to the entire document, I apply it just to the view element. i.e. add a this before your selector:
initColorPicker: ->
#.$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
That keeps the range of what you're doing with the plugin confined to the specific view you call the plugin from, which is important.
Your analysis is correct, that your code will not work unless your view is attached to the Dom. You do have multiple options now:
Move the event handling into the View via the events option. I would then also recommend to attach the event handlers to your view.
Attach the view to the dom during creation via the el option. Read this post, especially the section on "Decouple Views from other DOM elements"
Following shioyama's recommendation, is the 3rd option to fix your problem.
All 3 of those should fix your problem. But all 3 are best practice, so you might want to apply all 3.

Backbone Marionette emptyView template being added to the DOM twice

I'm use a Marionette Composite View with the emptyView property to render a simple template when the collection for the composite view has no models. emptyView works fine on multiple places in my app, but for some reason there's one view where the emptyView is rendering twice on the page.
My initial thought was the the view was re-rendering and not removing itself when the collection was synced. A console.log in the initialize function reveals that initialize is only being called once though.
class AllLists extends Backbone.Marionette.CompositeView
itemView: List
emptyView: NoLists
template: AllListsTemplate
className: 'lists'
initialize: (options) ->
#collection.fetch()
console.log 'lists ', #collection, #model
class NoLists extends Backbone.Marionette.ItemView
template: NoListsTemplate
tagName: 'li'
className: 'no-lists'
As you can see, there's nothing too crazy going on here. The empty list template is just a simple h4 tag with some text in it.
Any ideas as to what might be causing this?
This is a known bug in the v0.9.3 release, and is fixed in the up-coming v0.9.4 release.
The fix is currently in the dev branch as a release preview, if you would like to get it now https://github.com/derickbailey/backbone.marionette/tree/dev
And there are a few pull requests that provided fixes if you want to patch your version with code from one of them:
https://github.com/derickbailey/backbone.marionette/pull/175

Resources