I have a rather complex Backbone application and I'm not sure how to organize the views/templates. The application is a web-based email client. I'm really having trouble understanding how to make this sidebar.
The application sidebar is very similar to what you see in Apple Mail/Outlook. It's basically a folder browser. This view exists on each page.
I have two main issues:
How do I get the collection data into the sidebar view?
There are technically three "collections" that get rendered on the sidebar - accounts => mailboxes, and labels. So some sample code would look like this:
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
<div class="sidebar-subtitle">Gmail</div>
<div class="mailboxes" data-account-id="1">
<ul>
<li>Inbox</li>
<li>Sent</li>
...
</ul>
</div>
<div class="sidebar-subtitle">Yahoo</div>
<div class="mailboxes" data-account-id="2">
<ul>
<li>Inbox</li>
<li>Sent</li>
...
</ul>
</div>
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<li>Home</li>
<li>Todo</li>
</ul>
</div>
</div>
So theoretically I need to do something like this, no?
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
<% for account in #accounts.models: %>
<div class="sidebar-subtitle"><%= account.get('name') %></div>
<div class="mailboxes" data-account-id="<%= account.get('id') %>">
<ul>
<% for mailbox in account.mailboxes.models: %>
<li><%= mailbox.get('name') %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<% for label in #labels: %>
<li><%= label.get('name') %></li>
<% end %>
</ul>
</div>
</div>
The problem is that I can't pass both #accounts and #labels from the router. I realize I could use #options, but that seems messy and unbackbone. This sidebar needs to exist on each page. What should that look like on the Backbone stack? I don't think it should have its own router, since it's not really a "page".
Do I break things up into smaller views?
Should each mailbox be its own view? Should each mailbox be its own view? Each label its own view? I want to listen for events, such as a new message, and update a mailboxes unread count (for example). But how do I do this? How do I nest the views and handle all the object creation/fetching efficiently? If I am going to break things into smaller views, what should my sidebar view look like?
Sorry if I'm rambling. I've been looking at this for days and can't seem to find a good solution.
Using options is totally legit and Backbone-y. So I wouldn't fear utilizing it although there are plenty of other ways to go with this.
Yeah. I think the sidebar should be initialized inside your bigger page-view which gets called by the router.
I also think your mailboxes should be its own sub-view. Sounds like regardless of whether the mail is yahoo or google, each mailbox will have the same functionality (like minimizing) so it makes a lot of sense to create that view class and reuse it multiple times.
So what should this mailbox view take? (with respect to collection)
Again, you have choices. Throw in 1 large collection that has everything, or throw in each collection that represents the mail from a type of account...
If your collections are already separate (e.g. 1 for Google mail, 1 for Yahoo mail) then it would be easy to create a subview and pass the appropriate collection into each one. Even if it was not separate, I'd probably filter the collection down to only the models it needs before passing it in as a collection. This is what I would lean toward. I'm assuming that in each mailbox view there is never a situation where that mailbox needs access to any mail model not associated with a particular account. So it doesn't make sense to pass in anything you don't need.
As for labels, I'm not sure. It depends on what and how your labels come to be I suppose. For example, do you have your own label model that is basically a label tag associated with a mail model.id? Or are labels an attribute of a mail models that you're plucking?
UPDATE with sample code - Focus on passing data through view hierarchy
So I basically outline here a very common type of way of making child views inside of parent views and how to pass different data (read: in your case collections) through the hierarchy of views.
You can do this for even more child views (like maybe each mail model has it's own representative child view as a list item inside the mailbox view etc.) by simply repeating the pattern, utilizing the [options] argument of the View constructor and passing stuff through and accepting it on the inside.
In many parts of this code, I take a long winded approach so as to make it more transparent exactly which data is getting passed where. I slightly altered your mailbox template just to illustrate a point as to how you might add more child-views but you can certainly change it back. What you do with your MailboxView is up to you and your code (depending on your goals) should reflect that.
So without further ado, here is some sample code to chew on and think about. I whipped this together so there might be errors. I didn't actually see if it would execute.
Normally in a view, we'd probably define the view.el as the most outer element in the template and only include the inner parts but to keep with the templates you provided and to decrease the number of possible confusing extras in the code, I've left the templates as they are for the most part.
// Your templates
<script id="sidebar" type="text/template">
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
// Mailbox here
// Mailbox here
// Labels here
</div>
</script>
// I altered this template slightly just to illustrate a point on appending the model
// view data into your mailbox view. You might not want this but it's just for
// demonstration purposes
<script id="mailbox" type="text/template">
<div class="sidebar-subtitle">Gmail</div>
<div class="mailboxes" data-account-id="1">
<ul class="inbox"></ul> // Changed this
<ul class="sent"></ul>
</div>
</script>
<script id="labelBox" type="text/template">
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<li>Home</li>
<li>Todo</li>
</ul>
</div>
</div>
</script>
Your application BIG view, main view that encompasses all.
AppView = Backbone.View.extend({
initialize: function() {
// You might not instantiate your collections here but it is a good starting
// point for this demo. We'll be passing these through children view and
// utilizing them in the appropriate views.
// Let's assume these collections we instantiate already have models inside them.
// Your 3 Collections
this.gmail = new MailCollection();
this.yahoo = new MailCollection();
this.labels = new LabelCollection();
},
render: function() {
this.$el.html();
// We pass each collection into the SidebarView [options] hash. We can "pick-up"
// these collections in the sidebar view via this.options.xxx
// Render the sidebar
var sidebar = new SidebarView({
'gmail':this.gmail,
'yahoo':this.yahoo,
'labels':this.labels
});
// Render the sidebar and place it in the AppView... anywhere really
// but for simplicity I just append it to the AppView $el
this.$el.append(sidebar.render().el);
return this;
}
});
Your sidebar view:
SidebarView = Backbone.View.extend({
template: _.template($('#sidebar').html()),
initialize: function() {
// We passed the 3 collections into Sidebar view and can access
// them through the options. We could have instantiated them here
// but I passed them into the side to illustrate that using
// options for this kind of thing is a-okay.
this.gmail = this.options.gmail,
this.yahoo = this.options.yahoo,
this.labels = this.options.labels
// This is an array of sub-view mailboxes so when we close this view,
// it's easy to loop through the subviews and close both mailboxes
this.mailboxes = [];
},
render: function() {
// We render the sidebarView using the template
this.$el.html(this.template());
// We generate the sub-view mailbox views (gmail and yahoo)
var gmailView = new MailboxView({
'collection': this.gmail // We pass in only the gmail collection
});
var yahooView = new MailboxView({
'collection': this.yahoo // Pass in the yahoo mail collection
});
var labelView = new LabelboxView({
'collection': this.labels // Pass in the labels collection
});
// We push the views into our array
this.mailboxes.push(gmailView);
this.mailboxes.push(yahooView);
this.mailboxes.push(labelView);
// Render each view and attach it to this sidebar view
this.$el.append(gmailView.render().el);
this.$el.append(yahooView.render().el);
this.$el.append(labelView.render().el);
return this;
},
onClose: function() {
// Sample code of how we close out child views. When this parent view closes,
// it automatically cleans up the child views.
_.each(this.mailboxes, function(view) {
view.close(); // A special function that I use to close views
});
}
});
See Zombie View Cleanup for more details on the onClose() and close() methods I use. Particularly helpful once you start creating lots of views / sub-view relationships in your apps.
Your Mailbox View:
MailboxView = Backbone.View.extend({
template: _.template($('#mailbox').html()),
initialize: function() {
// When we pass something in as 'collection' backbone automatically
// attaches it as a property of the view so you can use the collection
// as this.collection rather than this.options.collection for
// convenience
},
render: function() {
this.$el.html(this.template());
this.loadMail();
return this;
},
loadMail: function() {
// Some code that loops through your collection and adds the mail to an
// appropriate DOM element. Would make sense to make Inbox a ul and each
// mail an li so I altered your template to demonstrate this
// Let's assume a mail model has the attr 'author' and 'body' in this
// simple example
// This will loop through your collection and use a mail template to
// populate your list with the appropriate data.
this.collection.each(function(mail) {
this.$('ul').append(_.template('<li><%=author%><%=body%></li>', mail.toJSON()))
});
// Alternatively, you could (and probably should) make each mail model
// represented by ANOTHER sub-view, a mail subview that has all the
// functionality that mail views usually have.
// To accomplish this you just repeat the prior demonstrated cycle
// of creating child view inside child view, passing in the appropriate
// data that you need, creating the view, and attaching it to the parent
// where you would like.
}
});
Your LabelsView:
LabelboxView = Backbone.View.extend({
template: _.template($('#labelBox').html()),
initialize: function() {
// I think you get the gist from above, you'd do the same thing as each
// mailbox view except deal with the unique aspects of labels, whatever
// these may be (and do)
},
render: function() {
this.$el.html(this.template());
return this;
}
});
Related
In the following code:
HTML
<div id="myView">
<button id="test_button">
Test Button
</button>
<ul id="output"></ul>
</div>
JavaScript
var myView = Backbone.View.extend({
initialize: function() {
// why doesn't this remove the previously delegated events?
this.undelegateEvents();
this.delegateEvents({
'click #test_button': 'buttonClicked'
});
},
// this event fires twice for one button click
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
new myView({el: "#myView"});
// instantiate view again
new myView({el: "#myView"});
});
why does
this.undelegateEvents();
in the initialize() method of the Backbone View not remove the previously delegated events from the previous instantiation of the View?
JSFiddle example of above code: https://jsfiddle.net/billb123/o43zruea/28/
I'll try not to shout but please stop trying to bind views to existing elements. Let the view create and own its own el, then call view.remove() to kill it off before replacing it. This simple change solves so many problems with view events that you should always think twice (and twice more) if you don't do it this way.
In your case, you'd have HTML like this:
<script id="t" type="text/x-underscore">
<div id="myView">
<button id="test_button">
Test Button
</button>
</div>
</script>
<div id="container">
</div>
<ul id="output"> <!-- This is outside the container because we're going to empty and refill it -->
</ul>
And your JavaScript would look like this:
var myView = Backbone.View.extend({
events: {
'click #test_button': 'buttonClicked'
},
render: function() {
this.$el.html($('#t').html());
return this;
},
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
var v = new myView();
$('#container').append(v.render().el);
v.remove(); // <----------------- Clean things up before adding a new one
v = new myView();
$('#container').append(v.render().el);
});
Points of interest:
Create the view then render it then put it on the page.
Call remove on the view when you're done with it.
The view goes inside the container. The caller owns the container, the view owns its el.
There are no delegateEvents or undelegateEvents calls anywhere. The presence of those almost always point to structural problems in your application IMO.
Each view is self contained: the outside world doesn't play with anything inside the view and the view keeps its hands to itself.
Updated fiddle: https://jsfiddle.net/bp8fqdgm/
But why didn't your attempted undelegateEvents do anything? undelegateEvents looks like this:
undelegateEvents: function() {
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
The cid is unique per view instance so each view instance uses its own unique namespace for events that delegateEvents binds. That means that this:
this.undelegateEvents();
this.delegateEvents();
is saying:
Remove the events that this instance of the view has bound. These events will be found in the the '.delegateEvents' + this.cid namespace where cid is unique for each view instance.
Bind the events that this instance of the view defines (or the events in the delegateEvents call). These events will be attached using the '.delegateEvents' + this.cid namespace.
So your undelegateEvents call is removing events but not all of them, only the specific event bindings that that view instance adds are removed.
Your this.undelegateEvents() call doesn't actually accomplish anything because it is in the wrong place and called at the wrong time. If the new View caller did the undelegateEvents call:
var v = new myView({el: "#myView"});
v.undelegateEvents();
new myView({el: "#myView"});
then it would happen in the right place and at the right time. Of course this means that your router needs to keep track of the current view so that it can currentView.undelegateEvents() at the right time; but if you're doing that then you'd be better off (IMO) taking the approach I outlined at the top of the answer.
I have a constant sidebar in my index.html file that lists projects using ng-include. When a project is created, or updated etc.. I would like the sidebar to automatically update along with it. I'm not sure which part of my code to provide, as hopefully it's a fundamental question that's easy to answer, though the solution eludes me.
Edit: feel I'm almost there, but src doesn't seem to pick up the controller property:
<div class="col col-md-4" data-ng-controller="ProjectsController" data-ng-include src="'{{sidebarUrl}}'"></div>
In my projects controller:
// Update existing Project
$scope.update = function() {
var project = $scope.project ;
project.$update(function() {
$location.path('projects/' + project._id);
$scope.$broadcast('projectUpdated');
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
$scope.sidebarUrl = 'modules/projects/views/list-projects.client.view.html';
$scope.$on("projectUpdated",function(event,args) {
$scope.sidebarUrl=null;
$scope.sidebarUrl= 'modules/projects/views/list-projects.client.view.html';
});
This is where services are your friend. You should start by encapsulating your CRUD operations into a service.
function MyCrudService($http, ...){ ... }
angular.module('my-app')
.service('myCrudService', MyCrudService);
Now, there are several ways you could implement the updating.
Use $rootScope and broadcast a message saying something has changed, and listen for that event in your sidebar controller (assuming you have one).
//Inside your service
function updateProject(proj){
//Update project
$rootScope.$broadcast('project-updated', proj);
}
//Inside your controller
function MySidebarController($scope){
$scope.$on('project-updated', function(){ ... });
}
Encapsulate the eventing logic inside your service to avoid using $rootScope. Just maintain your own list of callbacks and execute them.
//Inside your controller
function MySidebarController(myCrudService){
myCrudService.onProjectChanged(function(){ ... });
}
Expose the shared data on your service that can be databound to.
//Inside your controller
function MySidebarController($scope, myCrudService){
$scope.projects = myCrudService.projects;
}
Personally, I try to avoid $scope in my controllers, but using it for eventing is OK. Still, I might write some kind of directive that would allow me to execute an expression whenever an event fired in order to avoid it.
<my-event-binding event='project-updated' expression='sideBar.updateProjects()' />
Okay, so I had the same requirement(dynamically changing menu items in an included side panel) what I did was to use a controller in the ng-include template. The template would then fetch the relevant menu items from a service and update the controller. The view had an ng-repeat directive to show all the menu items (projects in your case).
<div ng-controller="ProjectsCtrl">
<ul>
<li ng-repeat="project in projects">
<a ng-href="project.url">
{{project.name}}
</a>
</li>
</ul>
</div>
The controller function could look something like:
function($scope, projectsSvc){
$scope.projects = [];
loadProjects();
$scope.$on("updatedProjects", loadProjects);
function loadProjects(){
projectsSvc.getProjects.success(function(projects){
$scope.projects = projects;
});
}
}
Projects are fetched from a service. When you update a project, broadcast an event that triggers a load of the projects again.
So after the new projects have been committed into the service backend, the sidebar will update accordingly.
I have a CompositeView with a list of ItemViews. The same collection is rendered for both states, when a user is logged in, and when a user is logged out.
The ItemView looks roughly like:
<div class="title">
{{ title }}
</div>
{{#if MA.currentUser }}
Add Review
{{/if}}
With the JavaScript:
MA.Views.Items.Movie = Backbone.Marionette.ItemView.extend({
template: 'items/movie',
className: 'movie'
});
However, this does not seem to display the expected 'Add Review'.
Any suggestions what could be done in this situation?
My understanding is that the template will only have access to the data that is passed to it. By default this is the model which is serialized by serializeData in the View Class which ItemView inherits. You can add additional data by using templateHelpers or writing a custom serializeData(may be other ways also).
Template Helpers
template helpers in a ItemView could be like:
templateHelpers: {
MA: function(){
return MA; //Unknown if having the same function name as the global will effect it
}
}
In your item view. See marionette docs template helper
Template helpers can be a function, object literal or object.
serializeData
For serializeData you just overwrite the serializeData method in your ItemView's Class.
serializeData: function(){
return {
"some attribute": "some value"
}
Worth noting that this would require you to serialize your model also. marionette docs serializeData
Conclusion
templateHelpers may turn out to be the simplest answer and probably what they were intended for.
Is it conventional to use Marionette.ItemView for view classes that do not have a specific model property associated with them?
As Marionette.View is not meant to be used directly, it seems like an ItemView makes sense as a view class with convenient defaults and bindings.
Or, should one just resort to using Backbone.View? If so, is there a way to hook Backbone.View into Marionette's evented and garbage-collected architecture?
Thank you for clarification!
ItemView can be used without a model. I do this quite regularly.
If you need to specify data for an ItemView, but not have that data in a Backbone.Model, you need to override the serializeData method:
MyView = Marionette.ItemView.extend({
serializeData: function(){
return {
my: "custom data"
};
}
});
the base Marionette.View isnt' meant to be used directly because it doesn't provide a render function on it's own. That doesn't mean you can't use it to create your own base view types, though. You could, for example, build a view type for your application that deals with rendering google maps or a third party widget or something else that doesn't need the general Backbone.Model based rendering that ItemView has in it.
I just found out you can use a templateHelper for this - just chuck this in your ItemView declaration:
templateHelpers: function() {
return {
message: this.message,
cssClass: this.cssClass
}
}
And then in your template:
<script type="text/html" id="notice-template">
<span class="<%= cssClass %>"><%= message %></span>
</script>
And then when you initialise the view:
var noticeView = new App.Views.Notice();
noticeView.message = "HELLO";
App.noticeRegion.show(noticeView);
I would be interested in your thoughts on this Derick?
I do an extensive use of templates, and I like to use full contained templates. I mean that I want to see in the template code all the DOM elements including the root one, like this:
<script type="text/template" id="template-card">
<div class="card box" id="card-<%= id %>">
<h2><%= title %></h2>
<div><%= name %></div>
</div>
</script>
But what Backbone likes is having a template like this:
<script type="text/template" id="template-card">
<h2><%= title %></h2>
<div><%= name %></div>
</script>
And defining the root element and its attributes in the JS code. What I think is ugly and confusing.
So, any good way to avoiding my Backbone View to wrapper my template with an extra DOM element?
I have been checking this issue thread: https://github.com/documentcloud/backbone/issues/546 and I understand there is not any official way to do it.. but maybe you can recommend me a non official way.
You can take advantage of view.setElement to render a complete template and use it as the view element.
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will
also create the cached $el reference and move the view's delegated
events from the old element to the new one
Two points you have to account for:
setElement calls undelegateEvents, taking care of the view events, but be careful to remove all other events you might have set yourself.
setElement doesn't inject the element into the DOM, you have to handle that yourself.
That said, your view could look like this
var FullTemplateView = Backbone.View.extend({
render: function () {
var html, $oldel = this.$el, $newel;
html = /**however you build your html : by a template, hardcoded, ... **/;
$newel = $(html);
// rebind and replace the element in the view
this.setElement($newel);
// reinject the element in the DOM
$oldel.replaceWith($newel);
return this;
}
});
And a working example to play with http://jsfiddle.net/gNBLV/7/
Now you can also define a view's tagName as a function and create a class like this:
var MyView = Backbone.View.extend({
template: '#my-template',
tagName: function() {
// inspect the template to retrieve the tag name
},
render: function() {
// render the template and append its contents to the current element
}
});
Here's a working example
Backbone.Decarative.Views provides you with an alternative way to do this, without having to rely on setElement. For more, check out my answer here.