Render jQuery object through Backbone View - javascript

I'm rather new to Backbone.js development, and have run into a bit of a roadblock while attempting to render a subview.
Currently, I have in place several views to render a custom dropdown-button, as well as other elements. I've taken this approach based on DocumentCloud's code
Here's what I have so far:
app.ui.SelectMenu = Backbone.View.extend({
className: 'btn-group group-item',
options: {
id: null,
standalone: false
},
events: {
"click .dropdown-menu a": "setLabel"
},
constructor: function (options) {
Backbone.View.call(this, options);
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
},
setLabel: function (label) {
$(this._label).text(label || this.options.label);
},
addItems: function (items) {
this.items = this.items.concat(items);
var elements = _(items).map(_.bind(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
var el = this.make('li', attrs, item.title);
return el;
}, this));
$(this.itemsContainer).append(elements);
}
});
So far I have successfully rendered my button, as well as the appropriate label, but I cannot seem to populate the .dropdown-menu when calling the addItems function.
I'm assuming that when render hits, the items variable cannot be populated due to the fact that I am passing a jQuery object and not a string, yet whenever I use items: this.itemsContainer.html(), that simply pastes the html surrounded by quotes... I could simply replace the quotes but that just feels like a hack to me.
Any help would be much appreciated. Thanks!

jQuery's append doesn't take an array:
.append( content [, content] )
content: DOM element, HTML string, or jQuery object to insert at the end of each element in the set of matched elements.
content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
If you want to append multiple elements in one call, you have to supply them as separate arguments:
$(x).append(e1, e2, e3, ...);
so you'd have to use apply to convert your array to separate arguments:
var $i = $(this.itemsContainer);
$i.append.apply($i, elements);
That sort of chicanery really isn't necessary though, you can add them one by one as you create them:
addItems: function (items) {
this.items = this.items.concat(items);
_(items).each(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
this.itemsContainer.append(this.make('li', attrs, item.title));
}, this);
}
Also note that _.each can take a context argument so you don't need a separate _.bind call. And I'm pretty sure that this.itemsContainer is already a jQuery object so you don't need to wrap it $() again.
You might have problems with your render as well:
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
}
I suspect that items: this.itemsContainer is going to end stringifying this.itemsContainer, you might have better luck with something like this:
this.$el.html(this.content.render({ label: this.options.label });
this.$el.find('some selector').append(this.itemsContainer);
where 'some selector' would, of course, depend on the HTML structure; you'll have to adjust the template for this as well.
Your Github link is broken so I don't know what code you're adapting. I do know that your use of constructor is non-standard. Why not use the standard initialize?
constructor / initialize new View([options])
[...] If the view defines an initialize function, it will be called when the view is first created.
You should probably do it this way:
app.ui.SelectMenu = Backbone.View.extend({
// No 'constructor' in here or anywhere...
initialize: function (options) {
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
//...
});

Related

Using Knockout bindings to return string

I'm trying to use Knockout to make the usage for an infinite scroll plugin I am using a bit nicer, but struggling with how to bind it. Not sure if it's even possible in the current form.
The scroller calls a data function which loads the next block of data via AJAX. It then calls a factory function that converts that data into HTML, and it then loads the HTML into the container, and updates its internal state for the current content size.
I'm stuck on the fact that it expects an HTML string.
What I want to do is this:
<div class="scroller" data-bind="infiniteScroll: { get: loadItems }">
<div class="item">
<p>
<span data-bind="text:page"></span>
<span class="info" data-bind="text"></span>
</p>
</div>
</div>
And my binding, which I'm completely stuck on, is this - which is currently just hardcoding the response, obviously - that's the bit I need to replace with the template binding:
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
var s = '';
for(var i = 0; i < contentdata.length; i++)
{
var o = contentdata[i];
// NEED TO REPLACE THIS
s += '<div class="item"><p>Item ' + o.page + '.' + i + ' <span class="info">' + o.text + '</span></p></div>';
}
return s;
};
$(el).infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};
contentdata is an array of objects e.g. [ { page:1, text:'Item1' }, { page:1, text:'Item2' } ... ]
Page sizes may differ between calls; I have no way of knowing what the service will return; it is not a traditional page, think of it more as the next block of data.
So in the element factory I want to somehow bind the contentdata array using the markup in .scroller as a template, similar to foreach, then return that markup to the scroller plugin.
Note that I can modify the infinite scroller source, so if if can't be done with strings, returning DOM elements would also be fine.
I just can't get how to a) use the content as a template, and b) return the binding results to the plugin so it can update its state.
NOTE: The page I eventually intend to use this is currently using a foreach over a non-trivial object model; thus the need to use the same markup; it needs to be pretty much a drop in replacement.
I have actually found out how to do it using the existing scroller following this question: Jquery knockout: Render template in-memory
Basically, you use applyBindingsToNode(domelement, bindings), which will apply KO bindings to a nodeset, which importantly does not have to be connected to the DOM.
So I can store the markup from my bound element as the template, then empty it, then for the element factory, create a temporary node set using jQuery, bind it using the above function, then return the HTML.
Admittedly, this would probably be better off refactored to use a pure KO scroller, but this means I can continue to use the tested and familiar plugin, and the code might help people as this seems to be quite a common question theme.
Here is the new code for the binding (markup is as above).
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
var template = $(el).html();
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
// Need a root element for foreach to use as a container, as it removes the root element on binding.
var newnodes = $('<div>' + template + '</div>');
ko.applyBindingsToNode(newnodes[0], { foreach: contentdata });
return newnodes.html();
};
$(el)
.empty()
.infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};

Backbone Not Firing Events

So here is an example of my app in jsfiddle: http://jsfiddle.net/GWXpn/1/
The problem is click event isn't being fired at all. I am not getting any JS errors in the console.
First, I wanted to display an unordered list with couple if items, each item should be clickable. This is what I did:
var FooModel = Backbone.Model.extend({});
var ListView = Backbone.View.extend({
tagName: 'ul', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render'); // every function that uses 'this' as the current object should be in here
},
render: function() {
for (var i = 0; i < 5; i++) {
var view = new SingleView({
model: new FooModel()
});
$(this.el).append(view.render().el);
}
return this; // for chainable calls, like .render().el
}
});
var SingleView = Backbone.View.extend({
tagName: 'li', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render', 'click'); // every function that uses 'this' as the current object should be in here
},
events: {
"click": "click"
},
click: function(ev) {
console.log("aaa");
alert(333);
},
render: function() {
$(this.el).append("aaa");
return this; // for chainable calls, like .render().el
}
});
I wanted to divide my app in to multiple modules (header, body, footer) so I created an abstract model and extended my modules from it:
var AbstractModule = Backbone.Model.extend({
getContent: function () {
return "TODO";
},
render: function () {
return $('<div></div>').append(this.getContent());
}
});
var HeaderModule = AbstractModule.extend({
id: "header-module",
});
var BodyModule = AbstractModule.extend({
id: "body-module",
getContent: function () {
var listView = new ListView();
return $("<div/>").append($(listView.render().el).clone()).html();
}
});
var ModuleCollection = Backbone.Collection.extend({
model: AbstractModule,
});
Then I just created my main view and rendered all its subviews:
var AppView = Backbone.View.extend({
el: $('#hello'),
initialize: function (modules) {
this.moduleCollection = new ModuleCollection();
for (var i = 0; i < modules.length; i++) {
this.moduleCollection.add(new modules[i]);
}
},
render: function () {
var self = this;
_(this.moduleCollection.models).each(function (module) { // in case collection is not empty
$(self.el).append(module.render());
}, this);
}
});
var appView = new AppView([HeaderModule, BodyModule]);
appView.render();
Any ideas why?
You have two bugs in one line:
return $("<div/>").append($(listView.render().el).clone()).html();
First of all, clone doesn't copy the events unless you explicitly ask for them:
Normally, any event handlers bound to the original element are not copied to the clone. The optional withDataAndEvents parameter allows us to change this behavior, and to instead make copies of all of the event handlers as well, bound to the new copy of the element.
[...]
As of jQuery 1.5, withDataAndEvents can be optionally enhanced with deepWithDataAndEvents to copy the events and data for all children of the cloned element.
You're cloning the <ul> here so you'll want to set both of those flags to true.
Also, html returns a string and strings don't have events so you're doubling down on your event killing.
I don't understand why you're cloning anything at all, you should just return the el and be done with it:
return listView.render().el;
If you insist on cloning, then you'd want something like this:
return $(listView.render().el).clone(true, true);
but that's just pointless busy work.
BTW, 'title' and 'Title' are different model attributes so you'll want to say:
console.log(this.model.get("title") + " clicked");
instead of
console.log(this.model.get("Title") + " clicked");
Also, Backbone collections have a lot of Underscore methods mixed in so don't mess with a collection's models directly, where you're currently saying:
_(this.moduleCollection.models).each(...)
just say:
this.moduleCollection.each(...)
And as Loamhoof mentions, 0.3.3 is ancient history, please upgrade to newer versions of Backbone, Underscore, and jQuery. You should also read the change logs so that you can use newer features (such as this.$el instead of $(this.el), fewer _.bindAll calls, listenTo, ...).
Partially Corrected Demo (including updated libraries): http://jsfiddle.net/ambiguous/e4Pba/
I also ripped out the alert call, that's a hateful debugging technique that can cause a huge mess if you get into accidental infinite loops and such, console.log is much friendlier.

Can't see my model within backbone collection

I'm trying to add an item to a collection but first I want to remove the existing one. Only one item will ever exist. I can create a new one, just not remove one. Maybe I'm doing it backwards.
This is my collection, the changetheme is the function that gets called, which works away, but can't figure out how to remove the existing one. this.model.destroy() just throws an error. Maybe i'm out of context.
bb.model.Settings = Backbone.Collection.extend(_.extend({
model: bb.model.Setting,
localStorage: new Store("rrr"),
initialize: function() {
var self = this
this.model.bind('add', this.added, this);
},
changetheme: function(value) {
var self = this
this.destroy();
this.create({theme:value});
},
}));
If it matters this is my model
bb.model.Setting = Backbone.Model.extend(_.extend({
defaults: {
theme: 'e'
},
initialize: function() {
var self = this;
},
added: function(item) {
var self = this;
this.destroy();
},
}));
To remove first item from collection you can call collection.shift(), also you can just clear collection by calling collection.reset(). So in your case one could write:
changetheme: function(value) {
this.shift();
this.create({theme:value});
}
UPD
Ok, let me explain - in your example localStorage plays like any other server side. So when you call "create", then according to docs backbone instantiates a model with a hash of attributes, saves it to the server(localStorage), and adds to the set after being successfully created. That is why your collection items count increases on each page refresh. But when you call shift/remove docs then only you client side collection is affected, not the server(localStorage) one. Now the best option for you to remove model both from server and client is calling model's destroy method like that:
changetheme: function(value) {
var modelToDelete = this.at(0) //take first model
modelToDelete.destroy();
this.create({theme:value});
}

Accessing a method on a Backbone.js view inside of loop

I'm working on an app that has an ItemListView that contains a number of ItemView elements. In my ItemListView, I'm using the jQuery .each() method to loop through the collection of items and render them as list elements.
I've got all the pieces in place except for the actual attaching of the li elements to the containing ul. The sticking point is getting access to the ItemListView.appendItem method from inside of my .each() loop. I've tried using this.appendItem and self.appendItem, but inside the loop this is the item and self is the window object.
Here's what I have right now:
ItemListView = Backbone.View.extend({
el: '#item-rows',
initialize: function () {
this.collection = new Items();
this.render();
},
render: function () {
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
this.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},
appendItem: function (itemView) {
$(this.el).append(itemView.render().el);
}
});
var itemlistview = new ItemListView;
I'm pretty sure that the context issue is the only problem, as I've examined the other pieces of the this by outputting them to the console and they look fine.
What am I missing?
A more Backbone-y way to do this would be to use the collection's each, provided by underscore.js:
render: function() {
this.collection.each( function( item, index ) {
var itemView = new ItemView( { model:item } );
this.appendItem( itemView );
}, this );
return this;
}
Notes:
notice the second param to each which take a reference to bind the function to
the each takes the element first and the index second
render should generally return this for chaining purposes (as mentioned in the docs), I don't think your appendItem function will work as you expect without this part
Yeah, it's a pretty simple fix. You just gotta refer to the this in the outer context.
render: function () {
var somereftothis = this;
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
somereftothis.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},

Creating methods on the fly

Hi I'm trying to author a jQuery plugin and I need to have methods accessible to elements after they are initialized as that kind of object, e.g.:
$('.list').list({some options}); //This initializes .list as a list
//now I want it to have certain methods like:
$('.list').find('List item'); //does some logic that I need
I tried with
$.fn.list = function (options) {
return this.each(function() {
// some code here
this.find = function(test) {
//function logic
}
}
}
and several other different attempts, I just can't figure out how to do it.
EDIT:
I'll try to explain this better.
I'm trying to turn a table into a list, basically like a list on a computer with column headers and sortable items and everything inbetween. You initiate the table with a command like
$(this).list({
data: [{id: 1, name:'My First List Item', date:'2010/06/26'}, {id:2, name:'Second', date:'2010/05/20'}]
});
.list will make the <tbody> sortable and do a few other initial tasks, then add the following methods to the element:
.findItem(condition) will allow you to find a certain item by a condition (like findItem('name == "Second"')
.list(condition) will list all items that match a given condition
.sort(key) will sort all items by a given key
etc.
What's the best way to go about doing this?
If you want these methods to be available on any jQuery object, you will have to add each one of them to jQuery's prototype. The reason is every time you call $(".list") a fresh new object is created, and any methods you attached to a previous such object will get lost.
Assign each method to jQuery's prototype as:
jQuery.fn.extend({
list: function() { .. },
findItem: function() { .. },
sort: function() { .. }
});
The list method here is special as it can be invoked on two occasions. First, when initializing the list, and second when finding particular items by a condition. You would have to differentiate between these two cases somehow - either by argument type, or some other parameter.
You can also use the data API to throw an exception if these methods are called for an object that has not been initialized with the list plugin. When ('xyz').list({ .. }) is first called, store some state variable in the data cache for that object. When any of the other methods - "list", "findItem", or "sort" are later invoked, check if the object contains that state variable in its data cache.
A better approach would be to namespace your plugin so that list() will return the extended object. The three extended methods can be called on its return value. The interface would be like:
$('selector').list({ ... });
$('selector').list().findOne(..);
$('selector').list().findAll(..);
$('selector').list().sort();
Or save a reference to the returned object the first time, and call methods on it directly.
var myList = $('selector').list({ ... });
myList.findOne(..);
myList.findAll(..);
myList.sort();
I found this solution here:
http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
This seems to do exactly what I need.
(function($) {
var TaskList = function(element, options)
{
var $elem = $(element);
var options = $.extend({
tasks: [],
folders: []
}, options || {});
this.changed = false;
this.selected = {};
$elem.sortable({
revert: true,
opacity: 0.5
});
this.findTask = function(test, look) {
var results = [];
for (var i = 0,l = options.tasks.length; i < l; i++)
{
var t = options['tasks'][i];
if (eval(test))
{
results.push(options.tasks[i]);
}
}
return results;
}
var debug = function(msg) {
if (window.console) {
console.log(msg);
}
}
}
$.fn.taskList = function(options)
{
return this.each(function() {
var element = $(this);
if (element.data('taskList')) { return; }
var taskList = new TaskList(this, options);
element.data('taskList', taskList);
});
}
})(jQuery);
Then I have
$('.task-list-table').taskList({
tasks: eval('(<?php echo mysql_real_escape_string(json_encode($tasks)); ?>)'),
folders: eval('(<?php echo mysql_real_escape_string(json_encode($folders)); ?>)')
});
var taskList = $('.task-list-table').data('taskList');
and I can use taskList.findTask(condition);
And since the constructor has $elem I can also edit the jQuery instance for methods like list(condition) etc. This works perfectly.
this.each isn't needed. This should do:
$.fn.list = function (options) {
this.find = function(test) {
//function logic
};
return this;
};
Note that you'd be overwriting jQuery's native find method, and doing so isn't recommended.
Also, for what it's worth, I don't think this is a good idea. jQuery instances are assumed to only have methods inherited from jQuery's prototype object, and as such I feel what you want to do would not be consistent with the generally accepted jQuery-plugin behaviour -- i.e. return the this object (the jQuery instance) unchanged.

Categories

Resources