How to properly use Marionette layouts? - backbone.js

my current code looks like this:
define([
'jquery',
'underscore',
'backbone',
'marionette',
'templates',
'gridView',
'detailView',
'detailModel'
], function ($, _, Backbone, Marionette, JST, GridView, DetailView, DetailModel) {
'use strict';
return Marionette.Layout.extend({
el: '#main',
template: JST['app/scripts/templates/main.ejs'],
initialize: function() {
this.render();
},
onRender: function () {
var Layout = Marionette.Layout.extend({
el: 'div',
template: _.template(""),
regions: {
grid: '#grid',
detail: '#detail'
}
});
this.layout = new Layout();
this.layout.render();
},
showGrid: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
var d = new DetailView(detailModel);
this.layout.grid.show(g);
this.layout.detail.show(d);
}
});
});
What I do not understand is why I need an extra layout in my onRender method to make this work. The '#grid' and '#detail' divs are part of the main.ejs template, but the following does not work:
return Marionette.Layout.extend({
el: '#main',
template: JST['app/scripts/templates/main.ejs'],
regions: {
grid: '#grid',
detail: '#detail'
},
initialize: function() {
this.render();
},
onRender: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
var d = new DetailView(detailModel);
this.grid.show(g);
this.detail.show(d);
}
});
It seems that the layout only works if the elements specified in the region object already exist when the layout is created. But the documentation says that this is not the case.
I'm probably doing something wrong. But what ?
Regards
Roger

In your second code example, try using onShow instead of onRender.
In addition, in Marionette you usually don't call render yourself, since the framework will call that method when you pass view/layouts to the show method.
You can see a different take on what you're trying to accomplish here :
https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js (particularly lines 43-46)

As an additional warning, calling .show() in the onRender method can negatively impact anything nested below that layout, especially if you are trying to use onShow later down the line to ensure that a view's DOM subtree is jQuery accessible.
.show() triggers a "show" event across any subviews of that layout and can mean that onShow() is called in those subviews (which listen for the "show" event) before those subviews have rendered and inserted their content.

Related

Backbone listenTo and 'this' context

I have a Backbone view where I listen for an event 'mouse:down' triggered by the view's canvas variable, which holds a fabric Canvas object. The event triggers the function 'myFunction', and inside the function I need to use 'this' to reference the view instance. See code below:
define([
'underscore',
'backbone',
'mustache',
'fabric'
], function(_, Backbone, Mustache, fabric) {
var MyView = Backbone.View.extend({
template: '<canvas id="my-canvas"></canvas>',
tagName: 'div',
id: 'my-id',
initialize: function () {
Mustache.parse(this.template);
},
render: function () {
this.$el.html(Mustache.render(this.template));
this.initCanvas('my-canvas');
return this;
},
initCanvas: function (canvasId) {
var canvas = new fabric.Canvas(canvasId, {
hoverCursor: 'pointer',
selection: false,
allowTouchScrolling: true,
width: 1170,
height: 658
});
fabric.Image.fromURL('myimage.jpg', function (img) {
canvas.setBackgroundImage(img);
canvas.renderAll();
});
this.canvas = canvas;
this.initCanvasListeners();
},
initCanvasListeners: function () {
this.listenTo(this.canvas, 'mouse:down', this.myFunction);
},
myFunction: function (options) {
console.log(this); // Outputs the canvas object, not MyView
}
});
return MyView;
});
'myFunction' is triggered, but now 'this' references the canvas object, not the view instance. How can I fix this? I need to call other functions of the view from 'myFunction', but I am quite stuck right now...
I have also unsuccessfully tried to change my event listener to look like this:
this.canvas.on('mouse:down', this.myFunction, this);
When setting up the event, bind it like this:
object.on(event, callback, [context])
Set context to the value object you want to be the "this" on the callback.
See: http://backbonejs.org/#Events-on
Thank you, nimgrg, that worked with a tiny modification:
this.canvas.on('mouse:down', this.myFunction.bind(this));

Backbone.js model trigger change only if model.on called again

It's my first post here so please be nice ;) I'm trying to create a Backbone+requireJS sample app and I have stumbled upon a strange problem. In my views initialize function I bind to models change event by
this.model.on("change", this.change());
Event is triggered correctly when data is loaded and all is fine. In my view I also bind to a click event. On click I try to change one of the properties and I was hoping for a change event, but it never came.
I was trying some stuff recommended here on stackoverflow (here, here, and there) but with no luck.
In my guts I feel it has to do with event binding. When I tried to bind again to models on change event inside click callback it started to work. So now I sit here and I'm, hm, confused. Could someone shed some light on this issue?
My View:
define(
[
'jquery',
'underscore',
'backbone',
'text!templates/news/newsListItem.html'
],
function($, _, Backbone, newsListItemTemplate)
{
var NewsListItemViewModel = Backbone.View.extend({
tagName: 'li',
events: {
"click a" : "onLinkClick"
},
initialize: function(){
this.model.on("change", this.changed());
},
render: function()
{
this.$el.html(_.template(newsListItemTemplate, this.model.toJSON()));
},
changed: function()
{
this.render();
console.log("changed");
},
//GUI functions
onLinkClick: function(e)
{
console.log("click!");
this.model.toggle();
this.model.on("change", this.changed());
}
});
var init = function(config){
return new NewsListItemViewModel(config);
}
return {
init : init
}
}
);
My Model:
define(
['jquery', 'underscore', 'backbone'],
function($, _, Backbone){
var NewsListItemModel = Backbone.Model.extend({
toggle: function() {
this.set('done', !this.get('done'));
this.trigger('change', this);
}
});
var init = function(data)
{
return new NewsListItemModel(data);
}
return {
init: init,
getClass: NewsListItemModel
};
}
);
Thanks for any input :)
First, you should use a function as event handler - not its result.
Hence, change the line into this:
this.model.on("change", this.changed.bind(this));
As it stands now, you actually fire this.changed() function just once - and assign its result (which is undefined, as the function doesn't have return statement) to be the model's change event handler.
Second, you shouldn't rebind this handler in onLinkClick: once bound, it'll stay here. It looks like it's more appropriate to trigger this event instead:
onLinkClick: function(e)
{
console.log("click!");
this.$el.toggle();
this.model.trigger('change');
}

Why won't my model get passed?

I have this collection view
define([
'jquery',
'underscore',
'backbone',
'views/project',
'collections/project-collection',
'templates'
], function ($, _, Backbone, ProjectView, ProjectCollection, JST) {
'use strict';
var ProjectListView = Backbone.View.extend({
template: JST['app/scripts/templates/projectList.ejs'],
el: $('#content'),
render: function() {
var projectCollection = new ProjectCollection();
projectCollection.fetch();
projectCollection.each(this.addOne(),this);
return this;
},
addOne: function(project) {
console.log('addOne function');
var projectView = new ProjectView({model: project});
this.$el.html( projectView.render().el);
}
});
return ProjectListView;
});
No matter what I try the model never gets passed through to the addOne function so the in the view that is instantiated by this method the call to
this.model.toJSON()
results in the old 'cannot call method .toJSON of undefined' error. I tried to inject the collection when this collection view was instantiated and that didn't work either. Obviously here it is in the dependency array and that doesn't work either. The model is definitely there as I can log projectCollection.model to the console inside the render function. I'm stumped.
I see two problems with your render: one you know about and one you don't.
The first problem is right here:
projectCollection.each(this.addOne(), this);
The parentheses on this.addOne() call the addOne method right there rather than passing the this.addOne function to each as a callback. You want this:
projectCollection.each(this.addOne, this);
The second problem is that you have to wait for the collection's fetch to return before anything will be in the collection. You can use the fetch's callbacks:
var _this = this;
projectCollection.fetch({
success: function() {
projectCollection.each(_this.addOne, _this);
}
});
or you can use the various events that fetch will fire, see the fetch documentation for details.

Creating backbone views with models from other views

Background:
I am making changes to an application that uses backbone.js with Handlebars as the templating engine. After a change event fires I need to create html that is appended to the current DOM structure which is basically just a spit-out of information that is contained in the model. This change needed to fit in the already established application structure.
Issue:
I have created a new view that uses a Handlebars template and the model to create the html. I then instantiate that view and call the render function and append the output using JQuery. What I am noticing is that when the html is rendered the model that is passed in because attributes on the $el instead of filling in the template (like I think it should).
View I'm altering:
$.hart.TestView = Backbone.View.extend({
tagName: "li",
template: Handlebars.compile($('#templateOne').html()),
initialize: function () {
this.model.on('change', function () {
this.createMoreInfoHtml();
}, this);
},
selectSomething: function () {
this.$el.removeClass('policies');
this.createMoreInfoHtml(); //function created for new view stuff
},
createMoreInfoHtml: function () {
var id = this.$el.attr('data-id', this.model.get("ID"));
$('.info').each(function () {
if ($(this).parent().attr('data-id') == id
$(this).remove();
});
var view = new $.hart.NewView(this.model, Handlebars.compile($("#NewTemplate").html()));
$('h1', this.$el).after(view.render().el);
},
render: function () {
... //render logic
}
});
View I Created:
$.hart.NewView = Backbone.View.extend({
initialize: function (model, template) {
this.model = model;
this.template = template;
},
render: function () {
this.$el.html(this.template({ info: this.model }));
this.$el.addClass('.info');
return this;
}
});
Json the is the model:
{
"PetName":"Asdfasdf",
"DateOfBirth":"3/11/2011 12:00:00 AM",
"IsSpayNeutered":false,
"Sex":"F",
"SpeciesID":2,
"ID":"ac8a42d2-7fa7-e211-8ef8-000c2964b571"
}
The template
<script id="NewTemplate" type="text/html">
<span>Pet Name: </span>
<span>{{this.PetName}}</span>
</script>
So now to the question: What am I doing wrong? Why are the properties of the model being created as attributes on the $el instead of filling in the template? Can someone please direct me as to how to get the results I am looking for?
Let's skip the problem Jack noticed.
The way you're creating your view is just wrong. It may work as you get the expected arguments in the initialize function, but it has unexpected behaviors you don't see. See the View's constructor:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
Now let's have a look at this _configure method:
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
And of course...
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
Ok here we are... Basically when passing the model as the options argument, you're passing an object with an attributes key (the attributes of your model). But this attributes key is also used in the View to bind attributes to its element! Therefore the behavior your noticed.
Now, other wrong thing. You're compiling your template each time you create a new function, but not using it as a singleton either. Put your template in the view:
$.hart.NewView = Backbone.View.extend({
template: Handlebars.compile($("#NewTemplate").html(),
And change your view's creation to make the whole thing work:
new $.hart.NewView({model: this.model});
Oh, and get rid of this useless initialize method. You're just doing things Backbone already does.

Accessing collection in multiple views - Backbone + RequireJS

I am working on my first RequireJS/Backbone app and I've hit a wall. There's a lot of code smell here, and I know I'm just missing on the pattern.
I have a route that shows all promotions, and one that shows a specific promotion (by Id):
showPromotions: function () {
var promotionsView = new PromotionsView();
},
editPromotion: function (promotionId) {
vent.trigger('promotion:show', promotionId);
}
In my promotions view initializer, I new up my PromotionsCollection & fetch. I also subscribe to the reset event on the collection. This calls addAll which ultimately builds a ul of all Promotions & appends it to a container div in the DOM.
define([
'jquery',
'underscore',
'backbone',
'app/vent',
'models/promotion/PromotionModel',
'views/promotions/Promotion',
'collections/promotions/PromotionsCollection',
'text!templates/promotions/promotionsListTemplate.html',
'views/promotions/Edit'
], function ($, _, Backbone, vent, PromotionModel, PromotionView, PromotionsCollection, promotionsListTemplate, PromotionEditView) {
var Promotions = Backbone.View.extend({
//el: ".main",
tagName: 'ul',
initialize: function () {
this.collection = new PromotionsCollection();
this.collection.on('reset', this.addAll, this);
this.collection.fetch();
},
render: function () {
$("#page").html(promotionsListTemplate);
return this;
},
addAll: function () {
//$("#page").html(promotionsListTemplate);
this.$el.empty().append('<li class="hide hero-unit NoCampaignsFound"><p>No campaigns found</p></li>');
this.collection.each(this.addOne, this);
this.render();
$("div.promotionsList").append(this.$el);
},
addOne: function (promotion) {
var promotionView = new PromotionView({ model: promotion });
this.$el.append(promotionView.render().el);
}
});
return Promotions;
});
Each promotion in the list has an edit button with a href of #promotion/edit/{id}. If I navigate first to the list page, and click edit, it works just fine. However, I cannot navigate straight to the edit page. I understand this is because I'm populating my collection in the initialize method on my View. I could have a "if collection.length == 0, fetch" type of call, but I prefer a design that doesn't have to perform this kind of check. My questions:
How do I make sure my collection is populated regardless of which route I took?
I'm calling render inside of my addAll method to pull in my template. I could certainly move that code in to addAll, but overall this code smells too. Should I have a "parent view" that's responsible for rendering the template itself, and instantiates my list/edit views as needed?
Thanks!
Here's one take. Just remember that there is more than one way to do this. In fact, this may not be the best one, but I do this myself, so maybe someone else can help us both!
First off though, you have a lot of imports in this js file. It's much easier to manage over time as you add/remove things if you import them like this:
define(function( require ){
// requirejs - too many includes to pass in the array
var $ = require('jquery'),
_ = require('underscore'),
Backbone = require('backbone'),
Ns = require('namespace'),
Auth = require('views/auth/Auth'),
SideNav = require('views/sidenav/SideNav'),
CustomerModel = require('models/customer/customer');
// blah blah blah...});
That's just a style suggestion though, your call. As for the collection business, something like this:
Forms.CustomerEdit = Backbone.View.extend({
template: _.template( CustomerEditTemplate ),
initialize: function( config ){
var view = this;
view.model.on('change',view.render,view);
},
deferredRender: function ( ) {
var view = this;
// needsRefresh decides if this model needs to be fetched.
// implement on the model itself when you extend from the backbone
// base model.
if ( view.model.needsRefresh() ) {
view.model.fetch();
} else {
view.render();
}
},
render:function () {
var view = this;
view.$el.html( view.template({rows:view.model.toJSON()}) );
return this;
}
});
CustomerEdit = Backbone.View.extend({
tagName: "div",
attributes: {"id":"customerEdit",
"data-role":"page"},
template: _.template( CustomerEditTemplate, {} ),
initialize: function( config ){
var view = this;
// config._id is passed in from the router, as you have done, aka promotionId
view._id = config._id;
// build basic dom structure
view.$el.append( view.template );
view._id = config._id;
// Customer.Foo.Bar would be an initialized collection that this view has
// access to. In this case, it might be a global or even a "private"
// object that is available in a closure
view.model = ( Customer.Foo.Bar ) ? Customer.Foo.Bar.get(view._id) : new CustomerModel({_id:view._id});
view.subViews = {sidenav:new Views.SideNav({parent:view}),
auth:new Views.Auth(),
editCustomer: new Forms.CustomerEdit({parent:view,
el:view.$('#editCustomer'),
model:view.model})
};
},
render:function () {
var view = this;
// render stuff as usual
view.$('div[data-role="sidetray"]').html( view.subViews.sidenav.render().el );
view.$('#security').html( view.subViews.auth.render().el );
// magic here. this subview will return quickly or fetch and return later
// either way, since you passed it an 'el' during init, it will update the dom
// independent of this (parent) view render call.
view.subViews.editCustomer.deferredRender();
return this;
}
Again, this is just one way and might be terribly wrong, but it's how I do it and it seems to work great. I usually put a "loading" message in the dom where the subview eventually renders with replacement html.

Resources