I have created a simple backbone app, which works well.
Now I need to solve a problem, that I'm having trouble with. So I seek advice.
I need to represent my models/collections read-only or editable,on a per user/group basis.
My initial thought was, create two templates (read and edit) and their respective views. (psuedo-code):
var appRouter = Backbone.Router.extend({
routes: {
'' : 'schedule',
}
schedule: function() {
this.collection = new Collection();
this.collection.fetch({success:function(resp) {
if (resp.group == 'allowedToEdit')
myview = new editView(this.collection);
else
myview = new readView(this.collection);
}});
});
This approach winds up with me having to duplicate templates:
<script type="text/template" id="edit-template">
<div class="myclass">
<input class="fn" type="text" value="<%= (fn) != '' ? fn : 'default' %>">
</div>
</script>
<script type="text/template" id="static-template">
<div class="myclass">
<div class="fn"><%= fn %></div>
</div>
</script>
Would it be better to in-line javascript for selecting an input or div tag instead or perhaps there is a better solution I'm not thinking of?
This is what I ended up doing:
I created a user model that contains user preferences and boolean values (returned from the database based on user permissions) if a user is allowed to edit or not.
The resulting code is:
var appRouter = Backbone.Router.extend({
routes: {
'' : 'schedule',
}
schedule: function() {
this.collection = new Collection();
this.collection.fetch({
success:function(resp) {
myview = user.schedule() ? new editView(this.collection)
: new readView(this.collection);
myview.render();
}});
});
The function user.schedule() just returns the boolean value that is associated with the requested route.
The permissions on the back-end are restricted by group - so if the user changes these boolean values manually
to access the editable page - they still don't have authorization on the back-end to manipulate data.
Also, in my case, the editable/static views are quite different so I created two separate templates.
Related
I'm pretty new to angular and have been having a bit of a problem in trying to create a basic "to-do" list sort of app.
There are various categories in the sidebar, and the user can click a button that brings up a modal prompting the user for the name of a new category. This name is used to create a new category, which is pushed onto the preexisting array.
However, the new category is only appearing after I start typing in another text-box on the screen or click on another tab.
The code that should be relevant:
var list = this;
$(document).on("click", ".prompt", function(e) {
bootbox.prompt("What do you want your new category to be?", function(result) {
if(result !== null) {
list.addCategory(result);
}
});
});
this.addCategory = function(result) {
if(result.trim() != "") {
var newCategory = new Category(result);
list.categories.push(newCategory);
this.setCategory(newCategory);
}
};
I can't seem to figure out how to post HTML as a code block, but here's the directives used to list out the categories (with categoryCtrl being the controller in question): ng-class="{active: categoryCtrl.isSet(category) }" ng-repeat="category in categoryCtrl.categories" ng-init="categoryCtrl.currCategory = categoryCtrl.categories[0]"
I know that the array is being updated immediately - if I add an alert 'bootbox.alert(list.categories[list.categories.length-1].name)' the alert gives me whatever the input was like it's supposed to. It's just not showing up in the ng-repeat.
Another interesting observations is that the ng-init overrides the this.setCategory(newCategory) - so it seems that when the list does update, it is reverting to its ng-init value.
Other places where I have an ng-repeat of an array, it's updated automatically when something new is pushed onto it. I'm wondering if it may have something to do with the modal/usage of bootbox - everywhere else things are added either by a checkbox or keying something into a textbox on screen, this is the only place where a modal is used.
Here is a working plunker based on your code.
The app looks like below. I initialize an array with dummy data for the example, but an empty array would work too. I used the vm = this syntax similar to what you have. When calling $nbBootbox.prompt it returns a promise so you need to use the then() syntax like below:
var app = angular.module('plunker', ['ngBootbox']);
app.controller('MainCtrl', ['$scope', '$ngBootbox', function($scope, $ngBootbox) {
var vm = this;
vm.name = 'World';
vm.categories = ['Category 1', 'Category 2'];
vm.prompt = function() {
$ngBootbox.prompt('Enter a new category?')
.then(function(result) {
console.log('Prompt returned: ' + result);
vm.categories.push(result);
}, function() {
console.log('Prompt dismissed!');
});
}
}]);
To make your HTML more angular like I changed it to this and also use the ControllerAs syntax:
<body ng-controller="MainCtrl as vm">
<p>Hello {{vm.name}} !</p>
<ul>
<li ng-repeat="c in vm.categories">{{c}}</li>
</ul>
Add Category
</body>
So, the link calls the prompt() function... it opens the modal and if you enter in the category, I push it to the categories array and it is added automatically to the page as a new bullet point in the list of categories.
From the documentation:
$ngBootbox.prompt(msg)
Returns a promise that is resolved when submitted and rejected if dismissed.
Example
$ngBootbox.prompt('Enter something')
.then(function(result) {
console.log('Prompt returned: ' + result);
}, function() {
console.log('Prompt dismissed!');
});
Hope this helps. let us know.
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;
}
});
I have a few different problems going on, I hope though this example is easy to follow. The code uses an HTML template with elements hidden by default (using CSS). The Backbone View uses data in a Model to display appropriate values OR hide the UI element if no value is present in the Mode.
Given a template where everything is hidden by default (using CSS), for example:
<script type="text/template" id="Person-Template">
<span class="fname" title="FirstName"></span>
<span class="lname" title="LastName"></span>
<span class="age" title="Age"></span>
</script>
To hide each UI element the CSS is:
span.fname,
span.lname,
span.age {
display:none;
}
My Backbone.js Model would therefore be:
PersonModel = Backbone.Model.extend({
defaults: {
fname: undefined,
lname: undefined,
age: undefined
}
});
The View (simplified) would be:
PersonView = Backbone.View.extend({
tagName: 'div',
initialize: function() {
this.model.on("fname", this.updateFName, this);
this.model.on("lname", this.updateLName, this);
this.model.on("age", this.updateAge, this);
},
updateFName: function() {
// Pseudo code
Get 'new' value from Model
Obtain reference to DOM element span.fname
Update span.fname
if (model value is empty) {
Hide UI element.
}
},
updateLName: function() {
// Same as above
},
updateAge: function() {
// Same as above
},
render: function() {
// Get model values to display
var values = {
FirstName : this.model.get('fname'),
LastName : this.model.get('lname'),
Age: this.model.get('age'),
};
// Load HTML template
var template = $('#Person-Template').html();
// Populate template with values
var t = _.template(template, values);
// Show / hide UI elements
this.updateFname();
this.updateLName();
this.updateAge();
}
}
Finally, the question: It seems hacky calling each updateXYZ() method from render() just to determine whether the UI element should be set to hidden or visible. I have a lot of attributes in my model and the code just seems a little absurd really.
I have been told on SO that the View should not be responsible for determining what should or should be displays. My questions is, well then what is responsible? The user may perform some (valid) aciton which clears the First Name, in which case I don't want my View displaying 'First name:' followed by no value.
First of all, you don't need to build your values by hand, just use toJSON:
var values = this.model.toJSON();
Then, you have to add your filled in template to your view's el:
this.$el.html(_.template(template, values));
and your template should probably include something to display in your template:
<script type="text/template" id="Person-Template">
<span class="fname" title="FirstName">F: <%= fname %></span>
<span class="lname" title="LastName">L: <%= lname %></span>
<span class="age" title="Age">A: <%= age %></span>
</script>
You don't separate functions for each of the three parts, you could just loop through them in your render:
_(values).each(function(v, k) {
var m = _(v).isUndefined() ? 'hide' : 'show';
this.$('.' + k)[m]();
}, this);
Now back to your events. There is no such thing as an "fname" event unless you've added a custom one. But there's no need for that, the model will trigger "change" and "change:fname" events when the fname is changed; you only need to care about "change" though:
initialize: function() {
_.bindAll(this, 'render');
this.model.on("change", this.render);
},
I've also bound render to your view instance using _.bindAll so that you don't have to worry about the third argument to this.model.on.
Now you have something that works: http://jsfiddle.net/ambiguous/46puP/
You can also push the "should this be displayed" logic into the template:
<script type="text/template" id="Person-Template">
<% if(fname) { %><span class="fname" title="FirstName">F: <%= fname %></span><% } %>
<% if(lname) { %><span class="lname" title="LastName">L: <%= lname %></span><% } %>
<% if(age) { %><span class="age" title="Age">A: <%= age %></span><% } %>
</script>
and simplify your render:
render: function() {
var values = this.model.toJSON();
var template = $('#Person-Template').html();
this.$el.html(_.template(template, values));
return this;
}
Demo: http://jsfiddle.net/ambiguous/W9cnJ/
This approach would probably be the most common and there's nothing wrong with it. I think you're misunderstanding what the previous answer was trying to tell you. The template chooses what pieces of information to display through <%= ... %> already so there's no good reason that it shouldn't see if fname, for example, is set before trying to display it. Depending on the nature of your data, you might want to use if(!_(fname).isUndefined()) and such in your template but a simple truthiness check is probably fine; the age might be an issue in some cases though so you might want to be a bit stricter with that.
I am using backbone.js,
I have a model that is formatted like this:
{
username:"name",
id:"1",
picture:"image.jpg"
}
I want to have a view like this:
<div id="1">
<span class="name">name<span>
<img src="image.jpg" />
</div>
from this template:
<script type="text/template" id="users-template">
<span class="name"><%= username %></span>
<img src="<%= image %>" />
</script>
but I get stuck when it comes to putting the users id into the views id attribute.
UserView = Backbone.View.extend({
id: this.model.id, //this is what I have tried but it doesnt work
initialize: function(){
this.template = _.template($('#users-template').html());
},
render: function(){
...
}
})
does anyone know how put the current models id into the id attribute?
I use that
id : function () {
return this.model.get("id");
}
U can get the model's data only using ".get()"
Properties like tagName, id, className, el, and events may also be defined as a function, if you want to wait to define them until runtime. #from Backbonejs.org
First of all, your model has picture but your template uses image, they should be the same or you'll get a "missing variable" error from your template.
The id attribute on a view is supposed to be the DOM id of an existing element; Backbone won't add it to the el, Backbone uses it to find the el. Besides, this won't be a view when id: this.model.id is executed (this will probably be window or some other useless thing) and the model property isn't set until the view is instantiated anyway.
The view's el is:
created from the view's tagName, className, id and attributes properties, if specified. If not, el is an empty div.
So drop the id from your view and use the default <div> for the el. Then, in your render method, you can set the id attribute on this.el; also, using a numeric id attribute causes trouble with some browsers so you should usually prefix it the id with something non-numeric:
render: function() {
this.el.id = 'v' + this.model.get('id');
this.$el.append(this.template(this.model.toJSON()));
return this;
}
Demo: http://jsfiddle.net/ambiguous/ZBE5z/
Open your console when running the demo and you'll be able to see the HTML that ends up in your view's this.el.
This happens because the model isn't set when the JavaScript initially gets executed. Try this:
initialize: function(){
this.id = this.model.id;
this.template = _.template($('#users-template').html());
},
From the backbone documentation:
All views have a DOM element at all times (the el property), whether they've already been inserted into the page or not.
I have following very simple javascript file:
CBBItem = Backbone.Model.extend(
{
});
CBBTrackItem = Backbone.View.extend(
{
template: _.template("<span><%= title %></span>"),
initialize: function()
{
_.bindAll(this, "render");
},
render: function()
{
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
And a html page like this:
<script type="text/javascript">
$(function()
{
var itm1 = new CBBItem({ title: 'track 1'});
var itmUI1 = new CBBTrackItem({ model: itm1, id: "kzl" });
itmUI1.render();
});
</script>
<body>
<div id="kzl"></div>
</body>
My view item doesn't want to render although there is a created div on the page. I can trick the situation in many ways. For example doing something like this
var itm1 = new CBBItem({ title: 'track 1'});
var itmUI1 = new CBBTrackItem({ model: itm1, id: "big_kzl" });
$(itmUI1.render().el).appendTo("#kzl");
But, why is the main case not working?
Here's one possibility: you aren't setting the el for the view, so it doesn't know what to do with your template. Could you modify your view-calling code to look like this?
var itmUI1 = new CBBTrackItem({
model: itm1,
id: "big_kz1",
el: "#kz1"
});
itmUT1.render();
Alternatively, you could set the el value within the initialize of the view if the value never varies. The advantage to doing so is that callers of the view don't have to know this information and thus the view is more self-contained.
If the document already has the element you want to use as el for a particular view, you have to manually set that dom element as the el attribute when the view is initialized. Backbone provides you no shortcut for doing that.
I've experienced problems when passing values like ID and events in during construction as opposed to defining them during extension. You may want to check and see if that's the difference you're looking for.