Backbone / Marionette controller inserting view into wrong region - backbone.js

I'm following Brian Mann's tutorial on building an application in Backbone + Marionette (~35min into Ep08), and I'm having problems with a controller inserting content into the main region rather than one of its sub-regions.
The page layout is structured like so:
<div id="header-region"></div>
<div id="main-region"></div>
<div id="footer-region"></div>
And I'm trying to insert content into an #admin-navs-region, which is to be nested under the #admin-list region inside of the #main-region:
<!-- admin/list/templates/list_layout; inserted into #main-region -->
<div id="admin-list">
<div id="banner-region"></div>
<div id="admin-navs-region"></div>
<div id="article-region"></div>
</div>
The controller in question is as follows:
#PE.module "AdminApp.List", (List, App, Backbone, Marionette, $, _) ->
class List.Controller extends App.Controllers.Application
initialize: ->
adminNavs = App.request "admin:nav:entities"
#layout = #getLayoutView()
#listenTo #layout, "show", => ## LINES IN QUESTION
#bannerRegion() ## LINE 11
#listRegion adminNavs ## LINE 12
#show #layout
bannerRegion: ->
bannerView = #getBannerView()
#show bannerView, region: #layout.bannerRegion
listRegion: (adminNavs) ->
listView = #getListView adminNavs
#show listView, region: #layout.adminNavsRegion
getListView: (adminNavs) ->
new List.Navs
collection: adminNavs
getBannerView: ->
new List.Banner
getLayoutView: ->
new List.Layout
And the corresponding view:
#PE.module "AdminApp.List", (List, App, Backbone, Marionette, $, _) ->
class List.Layout extends App.Views.Layout
template: "admin/list/list_layout"
regions:
bannerRegion: "#banner-region"
articleRegion: "#article-region"
adminNavsRegion: "#admin-navs-region"
class List.Banner extends App.Views.ItemView
template: "admin/list/_banner"
class List.Nav extends App.Views.ItemView
template: "admin/list/_nav"
tagName: "li"
class List.Navs extends App.Views.CollectionView
tagName: "ul"
className: "side-nav"
itemView: List.Nav
When I comment out the LINES IN QUESTION (see 3rd code snippet), the list_layout is rendered properly within the #main-region. However, if I uncomment LINE 11 or LINE 12, #admin-list is being entirely replaced with the List.Banner or List.Nav templates, rather than inserting into the specified region.
Everything looks like it should be working to me, but instead of insertion into the correct region, the code is simply replacing everything inside of #main-region.
Does anyone know how to start debugging this?

Solved after realizing this is from some peculiar coffeescript / Backbone / Marionette interactions...
I'm using tabs (not spaces) to indent my coffeescript, and I had:
<tab> class List.Layout extends App.Views.Layout
<space><space><tab> template: "admin/list/list_layout"
For whatever reason, this doesn't break things but rather causes Marionette to render over the parent section. Fixed this by replacing the <space><space> with a <tab>:
<tab> class List.Layout extends App.Views.Layout
<tab><tab> template: "admin/list/list_layout"
Just one of those Mondays...

Related

Backbone Marionette Slow Composite Views (200+ collections)

When showing a dropdown composite view collection of around 200 countries my application gets far too slow.
What is the best way to increase performance when dealing with large collections in marionette composite views?
Here is the function in the controller that is very slow to load. It is fast with only the following lines removed:
#layout.shippingCountryRegion.show shippingCountryView
#layout.billingCountryRegion.show billingCountryView
So it appears to be a very slow rendering issue.
Show.Controller =
showProfile: ->
#layout = #getLayoutView()
#layout.on "show", =>
headerView = #getHeaderView()
#layout.headerRegion.show headerView
accessView = #getAccessView()
#layout.accessRegion.show accessView
billingReadmeView = #getBillingReadmeView()
#layout.billingReadmeRegion.show billingReadmeView
billingFieldsView = #getBillingFieldsView()
#layout.billingFieldRegion.show billingFieldsView
shippingReadmeView = #getShippingReadmeView()
#layout.shippingReadmeRegion.show shippingReadmeView
shippingFieldsView = #getShippingFieldsView()
#layout.shippingFieldRegion.show shippingFieldsView
MyApp.request "location:get_countries", (countries) =>
billingCountryView = #getBillingCountryView(countries)
#layout.billingCountryRegion.show billingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('billing_country_id'), (states) =>
billingStateView = #getBillingStatesView(states)
#layout.billingStateRegion.show billingStateView
MyApp.request "location:get_countries", (countries) =>
shippingCountryView = #getShippingCountryView(countries)
#layout.shippingCountryRegion.show shippingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('shipping_country_id'), (states) =>
shippingStateView = #getShippingStatesView(states)
#layout.shippingStateRegion.show shippingStateView
MyApp.mainRegion.show #layout
The billing country view:
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
onRender: ->
this.$el.attr('value', this.model.get('id'));
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
this.$el.attr('selected', 'selected');
class View.BillingCountryDropdown extends MyApp.Views.CompositeView
template: billingCountryTpl
itemView: View.BillingCountryDropdownItem
itemViewContainer: "select"
The template, simply:
<label>Country
<select id="billing_country_id" name="billing_country_id">
<%- name %>
</select>
</label>
Your code can be optimized. Just move content of onRender method to the ItemView attributes.
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
attributes: ->
var id = this.model.get('id');
var attributes = { 'value': id };
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
attributes['selected'] = 'selected';
return attributes
The difference between this method and onRender case is that, on render will execute when collection already rendered and 200+ operations will be done with DOM nodes, which will bring performance issues.
In case of attributes method, it executes upon view creation.
There are few advices you can follow:
1) Ask your self do you really need to render all items at once? Maybe you can render part of collection and render other items on scroll or use pagination or use 'virtual scrioll' with SlickGrid or Webix for example.
2) Checkout how often you re-render your view. Try to minify num of events cause re-render
3) Try to minify num of event listeners of ItemView. Its good practice to delegate context events to CollectionView
4) You can use setTimeout to render collection by parts. For example you divide you coll in 4 parts by 50 items and raise 4 timeouts to render it.
5) You can optimize underscore templating and get rid of with {} operator. http://underscorejs.org/#template
What's in your 'billingCountryItemTpl' varible? If it's just string with template ID then you could precompile your template using Marionette.TemplateCache.
So you'll have:
template: Marionette.TemplateCache.get(billingCountryItemTpl)

Can't access model attributes from Backbone View

I'm walking into a large Backbone.js project so I'm still getting my bearings. My template, my-group-item.jhbs has:
{{#if isComplete}}
.row-fluid
.span2
img.entity-image(src="/pictures/{{entityId}}.png")
.span10
.row-fluid
.span12
h3 {{entityName}}
p My first variable {{totalFirst}} and my second variable {{totalValue}}
{{/if}}
My View is:
module.exports = class MyItemView extends View
className: ->
templateData = #getTemplateData()
primaryData = #model.get('primaryData')
tagName: 'li'
template: require 'views/my-group-item'
initialize: () ->
super
primaryData = #model.get('primaryData')
In my template, the totalFirst and totalValue variables show nothing.
I'm calling my view with:
#groupView = new MyItemView
collection: groups
el: '.group-list'
How can I get these to show in the template?
You could pass more than 1 variable to your template, the first being your model and the second being the attributes of primaryData.

How to treat nested collection events in Marionette?

I have an API resource that gives me a list of users that each have several items. The hierarchy is like so:
- users
- user
- items
- item
- item
- item
- user
- items
- item
- item
- item
I would like to display the list of users on a single page, with each user entry displaying each of its items on the page as well.
When any one of these items is clicked, it should set an chosen attribute that is accessible through the overall users collection.
I'm having difficulty getting the item click information to bubble back up. My current implementation is creating a separate items collection in order to render the view, but then I lose the connection to its original user model, so I can't notify it when the item is selected.
My views are structured like so:
class List.Item extends Marionette.ItemView
template: "path/to/template"
events:
"click" : "choose"
choose: (e) ->
# what to do?
class List.User extends Marionette.CompositeView
collection: #collection
template: "path/to/template"
itemView: List.Item
itemViewContainer: "span"
initialize: ->
#collection = new App.Entities.Items(#model.get("items"), parent: #)
events:
"click a" : "toggleChoose"
#include "Chooseable"
class List.Users extends Marionette.CollectionView
itemView: List.User
Is there a better way to structure these collections or views, or is there a way to pass the information from the List.Item view to the parent List.User view and then into the users collection?
EDIT
I have tried backbone-relational, but it didn't seem to quite do what I need. Please correct me if I'm wrong here.
Your List.Item should contain it's current model with all properties at the time when choose is triggered. In this way, you can trigger other events with the List.Item's model values:
choose(e) : ->
trigger("mylistitem:choose", model)
Then listen for the event elsewhere :
itemView.on("itemview:mylistitem:choose", ( childView, model ) -> {
alert(model.get('..whatever..')
}
It is actually possible to instantiate the items collection to reference the parent user and vice-versa directly in Backbone:
class Entities.User extends Backbone.Model
...
initialize: ->
#items = new Entities.Items #get("items"),
user: #
class Entities.Items extends Backbone.Collection
...
initialize: (models, options) ->
#user = options?.user
So now the List.User CompositeView can pass this information to the List.Item ItemView:
class List.User extends Marionette.CompositeView
collection: #collection
...
initialize: ->
#collection = #model.items
With this in place, it is possible to access the user directly from the ItemView:
class List.Item extends Marionette.ItemView
...
events:
"click" : "choose"
choose: (e) ->
e.preventDefault()
user = #model.collection.user
console.log "user: ", user
And from there it's possible to take any necessary actions on the user and its collection.

View is not showing in backbone.marionette

I was trying to build an example to demonstrate the region and layout thing of Backbone.Marionette . But i stuck in the layout , though i am calling the layout.region.show() , it's not showing anywhere in the DOM.
Full example can be found in this JsFiddle.
This is the layout part :
AppLayout = Backbone.Marionette.Layout.extend({
template: "#layout-template",
el : "layout-containr",
regions: {
menu: "#menu",
content: "#content"
}
});
Layout template:
<script id="layout-template" type="text/template">
<section>
<div id="menu"></div>
<div id="content"></div>
</section>
Here is how i am showing the layout :
var layout = new AppLayout();
layout.render();
layout.menu.show(gridView);
GridView's definition can be found here:
var GridView = Backbone.Marionette.CollectionView.extend({
itemView: GridRow,
el:'#menu'
});
Full example can be found in this JsFiddle.
And i have a complimentary question :
How layout will know where it should be attached ???
I did not find it anywhere in the net which makes me sure that i am missing some concepts here.
You need a bigger region to show the layout, in application level.
Usually when you initialize a Marionette application, you will have some top-leve regions to show the layout you want to render later.
App.addInitializer =>
App.addRegions
menuRegion: '#header'
contentRegion: '#stage' # These DOM come from your application layout
And then in your controller, you show these layout in your top-leve region.
indexShow: ->
layout = new App.Layouts.SomeLayout()
App.contentRegion.show(layout)
someView = new App.Views.SomeView()
anotherView = new App.Views.AnotherView()
layout.someSubRegion.show(someView)
layout.anotherSubRegion.show(anotherView)
And, you usually don't need an el either in your view or layout

Getting "NoTemplateError: Could not find template" with backbone.marionette

In this coffeescript+backbone.marionette application I am trying to write, I am getting a "NoTemplateError: Could not find template: '#second-template' when I try to display a different view in my content region.
Here are the two pieces of code, which are based on David Sulc's Backbone Books tutorial. The WelcomeApp displays fine but when I click the menu item that then calls MyApp.SecondApp.display(), I get the NoTemplateError.
window.MyApp = MyApp = new Backbone.Marionette.Application()
MyApp.addRegions
menu: '#menu'
content: '#content'
class MyApp.MenuView extends Backbone.Marionette.View
el: '#menu'
events:
'click #get-second': 'showSecond'
showSecond: ->
MyApp.SecondApp.display()
MyApp.vent.on 'welcome:rendered', ->
menu = new MyApp.MenuView()
MyApp.menu.attachView(menu)
MyApp.WelcomeApp = do ->
WelcomeApp = {}
class WelcomeLayout extends Backbone.Marionette.Layout
template: '#content_welcome-template'
WelcomeApp.display = ->
WelcomeApp.layout = new WelcomeLayout()
WelcomeApp.layout.on 'show', ->
MyApp.vent.trigger 'welcome:rendered'
MyApp.content.show MyApp.WelcomeApp.layout
return WelcomeApp
MyApp.SecondApp = {}
class MyApp.SecondApp.WelcomeView extends Backbone.Marionette.ItemView
template: '#second-template'
MyApp.SecondApp.display = ->
welcomeView = new MyApp.SecondApp.WelcomeView()
MyApp.content.show welcomeView
MyApp.addInitializer ->
MyApp.WelcomeApp.display()
My templates are simply script blocks in the index.html. I actually swapped the template used by the WelcomeApp with that used by the SecondApp and the WelcomeApp can find '#second-template' fine when I do this.
I tried this with both backbone.marionette 0.9.10 and 0.9.11.
Any help here would be greatly appreciated!
Thanks to both commenters on my question. While working to strip down the HTML and coffeescript code, I noticed a div in my HTML that was incorrectly closed using <div>. Once I fixed that to a </div> everything worked. Stupid error but I was just not seeing it until I strip down the HTML enough that it was right in my face. I need better syntax checking.

Resources