Marionette.js ItemView - Put child view in region - javascript

I have the following ItemView template which is filled with customer data (firstName, lastName) and I want to add a CollectionView into the div .addresses.
Template
<script type="text/html" id="template-customer-details">
<h4><%= firstName %> <%= lastName %></h4>
<button class="edit">Edit</button>
<h5>Addresses</h5>
<div class="addresses">...</div>
</script>
Layout
Layout.Details = Backbone.Marionette.ItemView.extend({
template: '#template-customer-details',
regions: {
addresses: ".addresses"
},
serializeData: function () {
return this.model.attributes;
},
initialize: function () {
this.addressList = new App.Models.AddressList();
// Error!
this.regions.addresses.show(this.addressList);
this.bindTo(this, "render", this.$el.refresh, this.$el);
this.model.bind("change", this.render.bind(this));
}
});
I am getting the error "Uncaught TypeError: Object .addresses has no method 'show'."
Do I have to wait until the view is loaded?

I think you've got things a bit confused. An ItemView doesn't do anything with a regions property (you may be thinking of the Application class), so when you try to call this.regions.addresses.show that's the same as calling ".addresses".show.
I think you probably want to use a CompositeView in this case as it combines an ItemView (which you can use for your customer data) and a CollectionView which you can use for your AddressList. You'll also need to define a separate ItemView for an address (as a CollectionView just creates an ItemView for each item in a collection).
Something a little like this (which I've not tested, so may not be entirely correct):
AddressView = Backbone.Marionette.ItemView.extend({
template: '#addressTemplate'
});
Layout.Details = Backbone.Marionette.CompositeView.extend({
template: '#template-customer-details',
itemView: AddressView,
itemViewContainer: '.addresses'
});
// Then create your view something like this:
new Layout.Details({
model: new App.Models.CustomerDetails(),
collection: new App.Models.AddressList()
});
I also don't think you need to specifically bind the change & render events like in your example as marionette will usually take care of that (the same with your implementation of serializeData, which looks like it's vaguely the same as the default implementation)

Related

Using Backbone.Marionette, why can I not reference #ui elements when creating a new instance of a view?

I have created a simple view, MyView, that extends from ItemView. Then when I create the instance of MyView, I am trying to add references to UI elements within the view as well as events that use those UI elements.
HTML
<div id="container"></div>
<script type="text/template" id="my-template">
<p>This is a rendered template.</p>
<button data-ui="changeModelNameButton">Change Model Name</button>
</script>
JS
// Define a custom view that extends off of ItemView
var MyView = Marionette.ItemView.extend({
template: "#my-template"
});
// Instantiate the custom view we defined above
var view = new MyView({
el: "#container",
ui: {
changeModelNameButton: '[data-ui~=changeModelNameButton]'
},
events: {
'click #ui.changeModelNameButton': function() {
alert('here');
}
}
});
// Render the view in the element defined within the custom view instantiation method
view.render();
I am receiving the following error in the console:
Uncaught TypeError: Cannot read property 'changeModelNameButton' of
undefined
I noticed that if I move the ui declarations to the view definition, it works fine, but I would like to know why I can't add those when I create the instance. Is there no way to add them to the instance or am I missing something?
Note: I am using Backbone 1.3.3, Marionette 2.4.4, Underscore 1.8.3, and jQuery 3.1.1
Options overriding view's properties
Not all the options are automatically overriding the view class properties when passed on instantiation.
ui looks like it's not one of them.
Backbone will automatically apply the following (private) viewOptions on a view:
// List of view options to be set as properties.
var viewOptions = [
'model',
'collection',
'el',
'id',
'attributes',
'className',
'tagName',
'events'
];
In the view constructor, this is extended with the chosen options (source):
_.extend(this, _.pick(options, viewOptions));
How to pass the ui option?
You either need to put the ui hash in the view class definition, or to apply the ui hash yourself.
var MyView = Marionette.ItemView.extend({
template: "#my-template",
initialize: function(options) {
// merge the received options with the default `ui` of this view.
this.ui = _.extend({}, this.ui, this.options.ui);
}
});
Note that options passed to a view are available in this.options automatically.
What's the real goal behind this?
If you're going to mess around the ui and the events with its callbacks, it would be best to define a new view.
It looks like a XY problem where the real problem lies in the architecture of the app, but we can't help since you're asking about what's blocking you right now.

Marionette CompositeView in the select>option context

I'm trying to set up a simple Marionette's CompositeView. Here's what I want in the end:
%select#target-id
%option{:value => "#{#id}"} = #name
%option{:value => "#{#id}"} = #name
etc
In my CompositeView I specify the childViewContainer: select and I need to display both #name (for the readability) and #id (for the related event) in the options of this select. Due to the nature of default div element I can to speficfy tagName as option in my ItemView:
class New.TargetView extends App.Views.ItemView
template: "path/to/template"
tagName: 'option'
And in the template I can pass only the content of to-be-created option element: = #name. This works fine, Marionette creates an option element for each model and populates it with the name of the model. The problem is that I don't know how to pass an attributes as well, since I can't specify an attribute of the element that hasn't been created yet.
I've also tried to set an attributes property on the ItemView like this:
attributes:
value: "#{#id}"
And it technically works: the options are populated with the value="" attribute, but the content is undefined. Please advice.
I'm not sure about part when you use attributes. You should pass hash or function that will return hash as stated in Backbone view.attributes docs.
attributes:
value: "#{#id}"
In old money it works like this. Here is jsfiddle.
attributes: function () {
return {
value: this.model.id
};
}
CompositeView and ItemView are views that require a Marionette collection and model, respectively. When you create a CompositeView you pass a collection of models, each of which will be passed to the corresponding ItemView.
My guess is that it's not a problem with the template but with how you are setting up the data. As far as I know, there is no attributes option for ItemView, you have to either initialize the CompositeView with a properly formed collection or create the new attributes with the serializeData method on the ItemView.
In the first case, you will do something like this:
var SomeCompositeView = Marionette.CompositeView.extend({
template: "your-template-name",
childViewContainer: select
});
var SomeItemView = Marionette.ItemView.extend({
template: "your-other-template",
tagName: 'option'
});
// This collection can be retrieved from a database or anywhere else
// Just make sure that the models have the fields you want
var myCollection = new Backbone.Collection([{id: "someid1", name: "name1"}, {id: "someid2", name: "name2"}]);
var view = new SomeCompositeView({collection: myCollection});
On the second case you'll have something similar but on the ItemView you'll have:
var SomeItemView = Marionette.ItemView.extend({
template: "your-other-template",
tagName: 'option',
serializeData: function () {
return { someAttribute: "someValue" }
}
}
Just remember that for this second approach, the ItemView has to have access to the values you are returning. serializeData is only for reformatting data or performing operations on the data that you cannot perform on the template. The template will only have access to the variables you are returning from serializeData, independently of the original model.

Template two models in one view - Backbone/Marionette

I'm trying to use two models in one view, and template using both of them. I'm working with Marionette. Here is me initialization of the view:
main_app_layout.header.show(new APP.Views.HeaderView({
model: oneModel,
model2 : twoModel}
));
Here is my view:
APP.Views.HeaderView = Backbone.Marionette.ItemView.extend({
template : '#view_template',
className: 'container',
initialize: function() {
//This correctly logs the second model
console.log(this.options.model2);
}
});
And here is the template:
<script id="view_template" type="text/template">
<p>{{twoModel_label}} {{oneModel_data}}</p>
<p>{{twoModel_label2}} {{oneModel_data2}}</p>
</script>
It renders everything correctly using the oneModel data, but doesn't render the second, even though it logs it correctly. I'm using Mustache as my templating language.
Can anyone help?
You can override the serializeData method on your view and have it return both models' data:
APP.Views.HeaderView = Backbone.Marionette.ItemView.extend({
// ...
serializeData: function(){
return {
model1: this.model.toJSON(),
model2: this.options.model2.toJSON()
};
}
});
Then your template would look like this:
<script id="view_template" type="text/template">
<p>{{model1.label}} {{model1.data}}</p>
<p>{{model2.label}} {{model2.data}}</p>
</script>
try creating a complex model that contains the two models, this new model will have the other two as properties and you can acccess the properties of each one like this answer explains..
Access Nested Backbone Model Attributes from Mustache Template

Binding a Backbone Model to a Marionette ItemView - blocking .fetch()?

This is a 2 part question. 1) Is there a better way to render a model to a view asynchronously? I'm currently making the ajax request using the fetch method in the model (though I'm calling it explicitly upon initilization), then rendering the templated view using an application event, vent, which gets published from inside the model after the parse method is called. Cool but wonky? 2) Would a blocking fetch method be of use, and is it possible?
The application renders this to the page:
layout
navbar
index
Then it fetches the model and renders this:
layout
navbar
thing
1
something
somethingelse
But, if I don't use the vent trigger, it (expectedly) renders:
layout
navbar
thing
1
null
null
The html templates:
<!-- Region: NavBar -->
<script type="text/template" id="template-navbar">
<div id="navbar">
navbar
</div>
</script>
<!-- View: IndexView -->
<script type="text/template" id="template-index">
<div id="index">
index
</div>
</script>
<!-- View: ThingView -->
<script type="text/template" id="template-thing">
<div id="thing">
thing<br/>
<%= id %><br/>
<%= valOne %><br/>
<%= valTwo %><br/>
</div>
</script>
<!-- Region -->
<div id="default-region">
<!-- Layout -->
<script type="text/template" id="template-default">
layout
<div id="region-navbar">
</div>
<div id="region-content">
</div>
</script>
</div>
app.js:
window.App = { }
# Region
class RegionContainer extends Backbone.Marionette.Region
el: '#default-region'
# Called on the region when the view has been rendered
onShow: (view) ->
console.log 'onShow RegionContainer'
App.RegionContainer = RegionContainer
# Layout
class DefaultLayout extends Backbone.Marionette.Layout
template: '#template-default'
regions:
navbarRegion: '#region-navbar'
contentRegion: '#region-content'
onShow: (view) ->
console.log 'onShow DefaultLayout'
App.DefaultLayout = DefaultLayout
# NavBar (View)
class NavBar extends Backbone.Marionette.ItemView
template: '#template-navbar'
initialize: () ->
console.log 'init App.NavBar'
App.NavBar = NavBar
# Index View
class IndexView extends Backbone.Marionette.ItemView
template: '#template-index'
initialize: () ->
console.log 'init App.IndexView'
App.IndexView = IndexView
# Thing View
class ThingView extends Backbone.Marionette.ItemView
template: '#template-thing'
model: null
initialize: () ->
console.log 'init App.ThingView'
events:
'click .test_button button': 'doSomething'
doSomething: () ->
console.log 'ItemView event -> doSomething()'
App.ThingView = ThingView
# Thing Model
class Thing extends Backbone.Model
defaults:
id: null
valOne: null
valTwo: null
url: () ->
'/thing/' + #attributes.id
initialize: (item) ->
console.log 'init App.Thing'
#fetch()
parse: (resp, xhr) ->
console.log 'parse response: ' + JSON.stringify resp
# resp: {"id":"1","valOne":"something","valTwo":"somethingelse"}
#attributes.id = resp.id
#attributes.valOne = resp.valOne
#attributes.valTwo = resp.valTwo
console.log 'Thing: ' + JSON.stringify #
#
App.MyApp.vent.trigger 'thingisdone'
App.Thing = Thing
# App
$ ->
# Create application, allow for global access
MyApp = new Backbone.Marionette.Application()
App.MyApp = MyApp
# RegionContainer
regionContainer = new App.RegionContainer
# DefaultLayout
defaultLayout = new App.DefaultLayout
regionContainer.show defaultLayout
# Views
navBarView = new App.NavBar
indexView = new App.IndexView
# Show defaults
defaultLayout.navbarRegion.show navBarView
defaultLayout.contentRegion.show indexView
# Allow for global access
App.defaultRegion = regionContainer
App.defaultLayout = defaultLayout
# Set default data for MyQpp (can't be empty?)
data =
that: 'this'
# On application init...
App.MyApp.addInitializer (data) ->
console.log 'init App.MyApp'
# Test
App.modelViewTrigger = ->
console.log 'trigger ajax request via model, render view'
App.MyApp.vent.trigger 'show:thing'
App.timeoutInit = ->
console.log 'init timeout'
setTimeout 'App.modelViewTrigger()', 2000
App.timeoutInit()
# Event pub/sub handling
App.MyApp.vent.on 'show:thing', ->
console.log 'received message -> show:thing'
thing = new App.Thing(id: '1')
App.thingView = new App.ThingView(model: thing)
# I should be able to do this, but it renders null
# App.defaultLayout.contentRegion.show App.thingView
# Testing to see if I could pub from inside model..yes!
App.MyApp.vent.on 'thingisdone', ->
console.log 'received message -> thingisdone'
App.defaultLayout.contentRegion.show App.thingView
MyApp.start data
From a very basic standpoint, throwing aside the specific example that you've provided, here is how I would approach the problem and solution.
A Generic Problem / Solution
Here's a generic version of the problem:
You need to fetch a model by its id.
You need a view to render after the model has been fetched.
This is fairly simple. Attach the model to the view before fetching the data, then use the "sync" event of the model to render the view:
MyView = Backbone.View.extend({
initialize: function(){
this.model.on("sync", this.render, this);
},
render: function(){ ... }
});
myModel = new MyModel({id: someId});
new MyView({
model: myModel
});
myModel.fetch();
Things to note:
I'm setting up the model with its id, and the view with the model before calling fetch on the model. This is needed in order to prevent a race condition between loading the data and rendering the view.
I've specified generic Backbone stuff here. Marionette will generally work the same, but do the rendering for you.
Your Specific Needs
Blocking Fetch
Bad idea, all around. Don't try it.
A blocking fetch will make your application completely unresponsive until the data has returned from the server. This will manifest itself as an application that performs poorly and freezes any time the user tries to do anything.
The key to not doing this is taking advantage of events and ensuring that your events are configured before you actually make the asynchronous call, as shown in my generic example.
And don't call the fetch from within the model's initializer. That's asking for trouble as you won't be able to set up any views or events before the fetch happens. I'm pretty sure this will solve the majority of the problems you're having with the asynchronous call.
Events Between View And Model
First, I would avoid using MyApp.vent to communicate between the model and the view instance. The view already has a reference to the model, so they should communicate directly with each other.
In other words, the model should directly trigger the event and the view should listen to the event on the model. This works in the same way as my simple example, but you can have your model trigger any event you want at any time.
I would also be sure to the use bindTo feature of Marionette's views, to assist in cleaning up the events when the view is closed.
MyView = Backbone.Marionette.ItemView.extend({
initialize: function(){
this.bindTo(this.model, "do:something", this.render, this);
}
});
MyModel = Backbone.Model.extend({
doSomething: function(){
this.trigger('do:something');
}
});
myModel = new MyModel();
new MyView({
model: myModel
});
myModel.doSomething();
Other Items
There are some other items that I think are causing some problems, or leading toward odd situations that will cause problems.
For example, you have too much happening in the DOMReady event: $ ->
It's not that you have too much code being executed from this event, but you have too much code defined within this event. You should not have to do anything more than this:
$ ->
App.MyApp.start(data)
Don't define your Marionette.Application object in this event callback, either. This should be defined on its own, so that you can set up your initializers outside of the DOMReady callback, and then trigger them with the app.start() call.
Take a look at the BBCloneMail sample application for an example on rendering a layout and then populating its regions after loading data and external templates:
source: https://github.com/derickbailey/bbclonemail
live app: http://bbclonemail.heroku.com/
I don't think I'm directly answering your questions the way you might want, but the ideas that I'm presenting should lead you to the answer that you need. I hope it helps at least. :)
See Derick's new suggestion to tackle this common problem at: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported
In short, move the asynchronous code away from your views, which means you need to provide them with models whose data has already been fetched. From the example in Marionette's upgrade guide:
Marionette.Controller.extend({
showById: function(id){
var model = new MyModel({
id: id
});
var promise = model.fetch();
$.when(promise).then(_.bind(this.showIt, this));
},
showIt: function(model){
var view = new MyView({
model: model
});
MyApp.myRegion.show(view);
}
});

Backbone.js: nesting views through templating

Is it technically possible to nest views, using templating, something like that:
<%= new PhotoCollectionView({model:new PhotoCollection(model.similarPhotos)}).render().el) %>
I can put all the stuff in the render method as well, but templating gives much more room for flexibility and layout.
I tried the aforementioned variant, but all I get as a result on the screen is [HTMLDivElement].
If I try to extract just the HTML out ouf it, using jQuery's HTML, I get it rendered, but it turns out that the DOM nodes that get printed out are different from the ones that the views hold a reference to, because no interaction whatsoever with those DOM nodes is possible using the view instance. For instance if within the view I say $(this.el).hide(), nothing will happen.
What is the proper way, if any?
I typically render the parent view first. I then use the this.$('selector') method to find a child element that I can use as the el of the child view.
Here is a full example:
var ChildView = Backbone.View.extend({
//..
})
var ParentView = Backbone.View.extend({
template: _.template($('#parent-template').html()),
initialize: function() {
_.bindAll(this, 'render');
}
render: function() {
var child_view = new ChildView({ el: this.$('#child-el') }); //This refers to ParentView.
return this;
}
});
var v = new ParentView();
v.render();
The accepted answer has a major flaw, which is the fact that the ChildView is going to be re-initialised everytime it's rendered. This means you will lose state and potentially have to re-initialised complicated views on each render.
I wrote a blog about this here: http://codehustler.org/blog/rendering-nested-views-backbone-js/
To summarise though, I would suggest using something like this:
var BaseView = Backbone.View.extend({
// Other code here...
renderNested: function( view, selector ) {
var $element = ( selector instanceof $ ) ? selector : this.$el.find( selector );
view.setElement( $element ).render();
}
});
var CustomView = BaseView.extend({
// Other code here...
render: function() {
this.$el.html( this.template() );
this.renderNested( this.nestedView, ".selector" );
return this;
}
});
You do not need to extend the Backbone view if you don't want to, the renderNested method could be put anywhere you like.
With the code above, you can now initialise the ChildView in the initialisation method and then simply render it when render() is called.
Check out the Backbone.Subviews mixin. It is a minimalist mixin built for managing nested views and does not re-initialize the child views every time the parent is rendered.
I don't know about within a template itself, but I've done it with tables and lists before. In the outer template, just have the stub:
<script type="text/template" id="table-template">
<table>
<thead>
<th>Column 1</th>
</thead>
<tbody>
</tbody>
</table>
</script>
and for the individual items:
<%= field1 %>
then in your render method, just render the individual items and append them to the tbody element...
The decision to initialize a new object every time you render seems to me very inefficient.
Particularly this:
render: function() {
var child_view = new ChildView({ el: this.$('#child-el') }); //This refers to ParentView.
return this;
}
Ideally the Rendering of the parent should be something like
render: function() {
this.$el.html(this.template());
this.childView1.render();
this.childView2.render();
}
And the children creation should happen only when initializing the parent:
initialize: function() {
this.childView1 = new ChildView1(selector1);
this.childView2 = new ChildView2(selector2);
}
the problem is that we do not have selector1 and selector2 before rendering the parent template. This is where I am stuck at right now :)

Categories