How to append classNames/attributes when extending views - backbone.js

var MyView1 = Marionette.ItemView.extend({
className: "MyView1",
attributes: { 'data-view': 'MyView1' }
});
var MyView2 = MyView1.extend({
className: "MyView2",
attributes: { 'data-view': 'MyView2' }
});
MyView1 is <div class="MyView1" data-view="MyView1">
MyView2 is <div class="MyView2" data-view="MyView2">
How to make MyView2 =
<div class="MyView1 MyView2" data-xxx="MyView1" data-yyy="MyView2">?
If impossible, this is also ok
MyView1 = <div class="MyView1" data-view="MyView1"
MyView2 = <div class="MyView1" data-view="MyView1" data-another-attrib="MyView2">

className can be defined as a function and evaluated at runtime. From the Backbone docs:
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.
So you could look-up the result of the parent class's className and append the new class to it. Make sure you use _.result to evalutate the parent's className incase it is also a function.
var MyView2 = MyView1.extend({
className: function(){
return _.result(MyView2.__super__, 'className') + " MyView2";
},
attributes: { 'data-view': 'MyView2' }
});
Alternatively, you could just add the two classes to MyView2's className:
var MyView2 = MyView1.extend({
className: "MyView1 MyView2",
attributes: { 'data-view': 'MyView2' }
});
Update - from the comments. You can also dig out the className from MyView1 as well:
var MyView2 = MyView1.extend({
className: MyView1.prototype.className + " MyView2",
attributes: { 'data-view': 'MyView2' }
});

Related

Can't display Backbone/Underscore template in Jade view

I'm trying to link the template in my .jade view to my Backbone model, and it's just not displaying anything inside my template script.
My .jade view:
extends ../layout
block content
.page
script(type="text/template" id="createRecipeTemplate").
<div class="ingredients-pane">
<form id="ingredientForm">
*[form]*
</form>
*[etc.]*
</div>
script(src='/js/myBackboneFile.js')
myBackboneFile.js:
var Recipe = Backbone.Model.extend({
defaults: {*[defaults]*}
});
var Recipes = Backbone.Collection.extend({
url: '/api/recipes'
})
var recipes = new Recipes();
var RecipeView = Backbone.View.extend({
model: new Recipe(),
el: '.page',
initialize: function() {
this.template = _.template($('#createRecipeTemplate').html());
},
events: {
*[events]*
},
*[functions for my events]*,
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var RecipeView = new RecipeView();
Everything's linked up - if I type 'RecipeView' into the browser console, it gives me:
n {cid: "view2", $el: n.fn.init[1], el: div.page}
I've tried a few variations on the jade template syntax - putting pipes at the beginning of each line, using the dot after the script tag (as above), and just indenting normally (with the template in both standard HTML and Jade syntax each time), but no joy.
Am I missing something obvious?

Marionette views not adding a line break after each element

I'm trying to create a simple list using Marionette's CollectionView and ItemView.
iv = Marionette.ItemView.extend({
tagName: "li",
className: "card",
template: _.template("empty-template")
});
cv = Marionette.CollectionView.extend({
tagName: "ul",
className: "card-list",
childView: iv
});
collection = new Backbone.Collection([
{ name: "alice" },
{ name: "bob" },
{ name: "carol" }
);
view = new cv({collection: collection});
view.render()
This works just fine, creates the list with three items and no content (the template is empty). But the on html code produced everything is stuck together, with no line breaks. Like this:
<ul class="card-list"><li class="card"></li><li class="card"></li><li class="card"></li></ul>
Is there a way of getting this instead?
<ul class="card-list">
<li class="card"></li>
<li class="card"></li>
<li class="card"></li>
</ul>
jsFiddle
EDIT: Any type of space would do. The li are inline-block and need to be spaced to be able to use justify on the ul.
Ok this is a guess, but maybe you can just change the default handler of attaching the html. So your "cv" code will look like this:
cv = Marionette.CollectionView.extend({
tagName: "ul",
className: "card-list",
childView: iv,
attachBuffer: function(collectionView, buffer) {
collectionView.$el.append(buffer + '\n');
},
});

Model not updating on form submit, using Backbone + Stickit

I'm trying to use the backbone.stickit library to bind my form input to the model but can't seem to get the model to update correctly.
The keyup event appears to work correctly, i can see the value change if i use the "onSet" callback to display it:
bindings: {
'#firstName': {
observe: 'firstName',
onSet: function(val, options) {
$('#output').html(val);
}
}
}
Here is my code (Run it on jsfiddle):
HTML
<div id="view">
<form name="form" id="form">
<input id="firstName" type="text"/>
<input type="submit" id="submit"/>
</form>
<div id="output"></div>
</div>
JavaScript
var app = {
Model: Backbone.Model.extend({
firstName: 'test'
}),
View: Backbone.View.extend({
el: "#view",
initialize: function(){
this.model = new app.Model();
this.render();
},
bindings: {
'#firstName': 'firstName'
},
render: function(){
this.$el.html( this.template );
this.stickit();
},
events: {
"submit #form": "submitForm"
},
submitForm: function(e) {
e.preventDefault();
$('#output').html('output:'+this.model.firstName);
}
})
};
var view = new app.View();
The way of getting a model attribute is usally not by accessing the attribute name as an object property, the way you did it this.model.firstName. personally I know a very few cases of such implemntation. The so called right way to do that is by using get method:
this.model.get("firstName").
This will return the current model value.
I usually define getters and setters for each model I use, so I would do the following:
getFirstName: function(){
return this.get("firstName");
}
Just looks better and more "easy on the eyes" :) (but totally not a must)
Here's an update of your fiddle: http://jsfiddle.net/srhfvs8h/1/

why backbone view is not getting rendered

this is below is code which i am using for simple task add , view is not getting render, i am not able to find the mistake ,
<!doctype html>
<html lang="en">
<head>
<title>Calculator</title>
<link rel="stylesheet" href="styles/bootstrap/css/bootstrap.min.css">
<script src="js/libs/jquery-1.9.1.min.js"></script>
<script src="js/libs/underscore-min.js"></script>
<script src="js/libs/backbone-min.js"></script>
<script type="text/template" id="display-template">
<div class="row">
<div class="span4">
<%=content%>
</div>
</div>
</script>
<script language="javascript">
var cdate;
var tasks={};
var app = app || {};
// App view responsible for rendering app
app.TaskView = Backbone.View.extend({
el: $('#tasks'),
template: _.template($('#display-template').html()),
initialize: function () {
this.render();
},
render: function () {
console.log("render called");
console.log(this.template());
this.$el.html(this.template());
}
});
app.task = Backbone.Model.extend({
defaults:{
content:null
}
});
app.bUsers = Backbone.Collection.extend({
model : app.task,
initialize: function(models, args) {
this.bind('add', this.renderone);
this.bind('remove', this.destroy); },
renderone:function(user){
console.log(user);
var view = new app.TaskView({model: user});
},
destroy:function(user){
$(user.view.el).remove();
}
});
app.Users = new app.bUsers();
$(document).ready(function() {
cdate=new Date();
$("#cdate").html(new Date());
$("#pre").click(function(){
cdate=new Date(cdate.getTime()-(1*24*3600*1000));
$("#cdate").html(cdate);
});
$("#next").click(function(){
cdate=new Date(cdate.getTime()+(1*24*3600*1000));
$("#cdate").html(cdate);
});
$("#submit").click(function(){
if(tasks[cdate]==undefined) tasks[cdate]=[];
tasks[cdate].push($("#task").val());
// app.appView = new app.TaskView({
// model: new app.task({"content":$("#task").val()})
// });
var data ={"content":$("#task").val()};
app.Users.add(data);
});
});
</script>
</head>
<body>
<a id="pre" href="#">Prev</a>
<div id="cdate"></div>
<a id="next" href="#">Next</a>
<input type="text" id="task" ></input>
<input type="button" value="submit" id="submit" ></input>
<div id="tasks"></div>
</body>
Oye, you've got a few problems.
To answer your specific question, your render method of your view should take your view's model instance and get something from this.model.toJSON() it to get at its data to pass to the template method (toJSON really returns "JSONable" objects).
But that's not all.
Besides a few html issues, you also have stylistic problems.
Collections generally should not be concerned with views, only data (*). Views should be concerned with with collections and models. Collections communicate to views via event binding, which I see you are doing. However, for reuse purposes, you may have more than one combination views that might want to listen to events in the collection. By setting up the event binding in the collection, you've effectively limited your collection for only one use.
Views can do alot. There's not much reason to manually add DOM event handlers when you can code the view to do it for you.
I haven't written Backbone in a little while (not by choice!), but generally found it was a good idea to have a view dedicated to the collection, and then have a separate model view that the collection view might create or destroy based upon whatever events took place.
Here's a bit of a cleanup of your code to give you a starting example:
http://jsfiddle.net/jfcox/SmPNv/
HTML:
<a id="pre" href="#">Prev</a>
<div id="cdate"> </div>
<a id="next" href="#">Next</a>
<input type="text" id="task" />
<input type="button" value="add" id="submit" />
<div id="tasks"></div>
Backbone definitions:
var defs = defs || {};
//first define the data handlers
defs.Task = Backbone.Model.extend({
defaults: function () {
return {
content: null,
addDate: (new Date()).toString()
};
}
});
defs.Users = Backbone.Collection.extend({
model: defs.Task
});
// App view responsible for rendering app
defs.SingleTaskView = Backbone.View.extend({
//since we can't control where the js is loaded, go ahead and make the template inline for jsFiddle demo.
tagName: 'div',
template: _.template('<div class="row"> <div class="span4"><%=content%></div> <em><%=addDate%></em> <button class="remove"> remove</remove> </div>'),
events: {
"click button.remove": "remove"
},
initialize: function (opts) {
this.model.on('change', this.render, this);
},
render: function () {
console.log("render called");
var modelBare = this.model.toJSON();
return this.$el.html(this.template(modelBare));
},
remove: function () {
//removes from local collection, does not delete on server
//for that, you'd want `this.model.destroy`
this.collection.remove(this.model);
//removes this view's element.
this.$el.remove();
}
})
defs.TasksView = Backbone.View.extend({
el: 'body',
events: {
"click #pre": "doPrevious",
"click #next ": "doNext",
"click #submit ": "doSubmit"
},
cdate: null,
initialize: function (opts) {
this.cdate = new Date();
this.render();
this.collection.on('add', this.renderone, this);
},
render: function () {
$("#cdate").html(this.cdate.toString());
},
doPrevious: function () {
this.cdate = new Date(this.cdate.getTime() - (1 * 24 * 3600 * 1000));
$("#cdate").html(this.cdate.toString());
},
doNext: function () {
this.cdate = new Date(this.cdate.getTime() + (1 * 24 * 3600 * 1000));
$("#cdate").html(this.cdate.toString());
},
doSubmit: function () {
var data = {
"content": $("#task").val()
};
this.collection.add([data]);
},
renderone: function (userModel) {
var view = new defs.SingleTaskView({
model: userModel,
collection: this.collection
});
this.$el.find('#tasks').append(view.render());
}
});
The application, itself.
var app = app || {};
app.users = new defs.Users();
(function ($) {
$(document).ready(function () {
app.usersview = new defs.TasksView({
collection: app.users
});
});
})(jQuery);
(*) This is a guideline, not an absolute rule, of course. If you think a collection might work as some sort of workflow manager, etc, that might be fine, but that's an advanced topic.
Edit: I included the template inline, partially for reasons that I don't trust jsFiddle with inline "text" scripts. I'm not recommending any way to handle that, just that's how I did it here.

BackboneJS with HandelbarJS: How to access model functions?

I am exploring a setup with HandelbarsJS and Backbone.
This is part of my template:
<a href="#genre/{{ name }}" class="genre-item" data-genre="{{ name }}">
<i class="icon-chevron-{{#if is_selected }}down{{else}}right{{/if}}"></i>
{{ display_name }} ({{ total }})
</a>
Meaning: I want to render a different icon, depending on whether a model is selected or not.
However, I never get the 'icon-chevron-down', but always the 'icon-chevron-right' path.
Any ideas what I am missing?
EDIT
The selection of a Genre is working on model level like this:
MA.Models.Genre = Backbone.Model.extend({
defaults: {
selected: false
},
is_selected: function() {
return (this.get('selected') == true);
},
toggle: function() {
if (this.is_selected()) {
this.set('selected', false);
}
else
{
this.set('selected', true);
}
}
});
MA.Collections.Categories = Backbone.Collection.extend({
model: MA.Models.Genre
});
This could probably be simplified, but I am not getting the selection of the Genre from the remote service, but it is used just as a temporary state change.
Without seeing what is happening in your view it's hard to tell. But you probably have a render function that look's like:
HandlebarsTemplate['templatename'](this.model.toJSON());
toJSON by default only includes model attributes. Also handlebars won't evalute functions on the fly like that.
The easiest solution is to fix the template to look like:
<a href="#genre/{{ name }}" class="genre-item" data-genre="{{ name }}">
<i class="icon-chevron-{{#if selected }}down{{else}}right{{/if}}"></i>
{{ display_name }} ({{ total }})
</a>
I've changed is_selected to selected to use the attribute instead of the function.
The other option is to modify the toJSON function of the model to include the evaluated function:
MA.Models.Genre = Backbone.Model.extend({
defaults: {
selected: false
},
is_selected: function() {
return (this.get('selected') == true);
},
toJSON: function(options) {
var attrs = _.clone(this.attributes);
attrs.is_selected = this.is_selected();
return attrs;
}
});

Resources